From 941c9165a61977ec23f27e06bf9de74c2e990d0f Mon Sep 17 00:00:00 2001 From: Scribe Date: Wed, 12 Aug 2026 22:36:43 -0500 Subject: [PATCH] Consolidate network modes into a single template per OS Merge the separate existing-subnet and new-subnet templates into one template per operating system, selected by a new networkMode parameter: - ExistingSubnet (default): join an existing VNET and subnet, NSG on the NIC - NewSubnet: add a subnet to an existing VNET, NSG on the new subnet - NewVnet: create the VNET and subnet, NSG on the new subnet Windows previously only supported an existing subnet and now supports all three modes. Removed linux-router-newsubnet.bicep/.json, which the consolidated template replaces. Also: - Make scriptUri resolve safely when deployment().properties.templateLink is unavailable (local file and template spec deployments), falling back to the master branch instead of failing. - Add tools/publish-templatespecs.sh to publish both templates as versioned template specs, and cover tools/ in CI. - Name the NSG after the VM instead of a shared default-nsg, so two routers in the same resource group no longer collide. - Emit privateIpAddress, publicIpAddress and subnetId outputs. - Update README: network modes section, consolidated parameter tables, template spec guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cd9ed807-66b2-43e3-8bf9-ed7eb24a46dd --- .github/workflows/validate-templates.yml | 9 +- README.md | 137 ++++++++--- infra/arm/linux-router-newsubnet.json | 259 --------------------- infra/arm/linux-router.json | 278 ++++++++++++++--------- infra/arm/windows-router.json | 258 +++++++++++++-------- infra/bicep/linux-router-newsubnet.bicep | 211 ----------------- infra/bicep/linux-router.bicep | 230 ++++++++++++------- infra/bicep/windows-router.bicep | 214 +++++++++++------ tools/publish-templatespecs.sh | 74 ++++++ 9 files changed, 823 insertions(+), 847 deletions(-) delete mode 100644 infra/arm/linux-router-newsubnet.json delete mode 100644 infra/bicep/linux-router-newsubnet.bicep create mode 100644 tools/publish-templatespecs.sh diff --git a/.github/workflows/validate-templates.yml b/.github/workflows/validate-templates.yml index cb76261..2e70736 100644 --- a/.github/workflows/validate-templates.yml +++ b/.github/workflows/validate-templates.yml @@ -3,9 +3,9 @@ name: Validate templates on: push: branches: [master] - paths: ['infra/**', 'scripts/**', '.github/workflows/validate-templates.yml'] + paths: ['infra/**', 'scripts/**', 'tools/**', '.github/workflows/validate-templates.yml'] pull_request: - paths: ['infra/**', 'scripts/**', '.github/workflows/validate-templates.yml'] + paths: ['infra/**', 'scripts/**', 'tools/**', '.github/workflows/validate-templates.yml'] workflow_dispatch: permissions: @@ -54,10 +54,11 @@ jobs: # shebang with "bad interpreter: No such file or directory". - name: Reject CRLF line endings run: | - if grep -rlU $'\r' scripts/linux labs; then + if grep -rlU $'\r' scripts/linux labs tools; then echo "::error::Files above contain CRLF line endings; they must be LF." exit 1 fi - name: Check syntax - run: for f in scripts/linux/*.sh; do bash -n "$f"; done + run: | + for f in scripts/linux/*.sh tools/*.sh; do bash -n "$f"; done diff --git a/README.md b/README.md index 600d0c1..2717c4b 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,15 @@ - [Overview](#overview) - [Quick start](#quick-start) - [Repository structure](#repository-structure) +- [Network modes](#network-modes) - [Linux router](#linux-router) - - [Choosing a template](#choosing-a-template) - [Parameters](#linux-parameters) - [Windows router](#windows-router) - [Parameters](#windows-parameters) - [Network security defaults](#network-security-defaults) - [Using the router](#using-the-router) - [Deploying from the command line](#deploying-from-the-command-line) +- [Publishing as a template spec](#publishing-as-a-template-spec) - [Setup scripts](#setup-scripts) - [Lab deployment scripts](#lab-deployment-scripts) - [Working with the templates](#working-with-the-templates) @@ -57,13 +58,12 @@ These templates build a single-NIC virtual machine with **IP forwarding enabled* ## Quick start -Pick a template and deploy straight to the portal: +Pick a template and deploy straight to the portal. Each one can join an existing subnet, add a subnet to an existing VNET, or build the VNET from scratch — see [Network modes](#network-modes). | Template | Use when | Deploy | Visualize | |---|---|---|---| -| **Linux — existing subnet** | You already have the VNET **and** the subnet | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router.json) | -| **Linux — new subnet** | You have the VNET and want the template to create a dedicated NVA subnet | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router-newsubnet.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router-newsubnet.json) | -| **Windows — existing subnet** | You want a Windows Server router | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Fwindows-router.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Fwindows-router.json) | +| **Linux router** | You want an Ubuntu router with forwarding and SNAT | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Flinux-router.json) | +| **Windows router** | You want a Windows Server router | [![Deploy To Azure](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Fwindows-router.json) | [![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fdmauser%2FAzureVM-Router%2Fmaster%2Finfra%2Farm%2Fwindows-router.json) | > [!IMPORTANT] > Set **`allowSshFromAddressPrefix`** (Linux) or **`allowRdpFromAddressPrefix`** (Windows) to your own public IP, e.g. `203.0.113.4/32`. Standard SKU public IPs block **all** inbound traffic unless an NSG allows it. See [Network security defaults](#network-security-defaults). @@ -84,24 +84,48 @@ Pick a template and deploy straight to the portal: ├── labs/ │ ├── *.azcli End-to-end Azure CLI lab builds │ └── conf/ Large BGP route lists used for scale testing +├── tools/ Helper scripts for publishing template specs ├── docs/ Supporting notes └── README.md ``` --- -## Linux router +## Network modes -Deploys an Ubuntu router with a single NIC and IP forwarding enabled. The setup script enables IPv4 and IPv6 forwarding, disables ICMP redirects, configures `iptables` SNAT (masquerade) to the internet for private-range sources, and persists all of it across reboots with `netfilter-persistent`. +Both templates take a **`networkMode`** parameter that decides how the router attaches to the network, so a single template covers every starting point. -### Choosing a template +| `networkMode` | Virtual network | Subnet | NSG placement | +|---|---|---|---| +| `ExistingSubnet` *(default)* | Must already exist | Must already exist | On the **NIC**, so an NSG already attached to that subnet is never overwritten | +| `NewSubnet` | Must already exist | **Created** using `subnetAddressPrefix` | On the **new subnet** | +| `NewVnet` | **Created** using `virtualNetworkAddressPrefix` | **Created** using `subnetAddressPrefix` | On the **new subnet** | -| | `linux-router.json` | `linux-router-newsubnet.json` | -|---|---|---| -| Requires an existing VNET | Yes | Yes | -| Requires an existing subnet | Yes (`existingSubnet`) | No — creates it (`subnetName`, `subnetPrefix`) | -| NSG placement | On the **NIC**, so an existing subnet NSG is never overwritten | On the **new subnet** it creates | -| NSG created when `allowSshFromAddressPrefix` is empty | No | Yes, allowing RFC 1918 inbound only | +The parameters that apply to each mode: + +| Parameter | `ExistingSubnet` | `NewSubnet` | `NewVnet` | +|---|---|---|---| +| `virtualNetworkName` | Name of the existing VNET | Name of the existing VNET | Name of the VNET to create | +| `virtualNetworkAddressPrefix` | Ignored | Ignored | Address space of the new VNET | +| `subnetName` | Name of the existing subnet | Name of the subnet to create | Name of the subnet to create | +| `subnetAddressPrefix` | Ignored | CIDR of the new subnet | CIDR of the new subnet | + +> [!NOTE] +> In the two subnet-creating modes the NSG is always created, because the template owns the new subnet. In `ExistingSubnet` mode the NSG is only created when you set `allowSshFromAddressPrefix` / `allowRdpFromAddressPrefix`, so a deployment into an existing subnet never attaches an unexpected NSG. + +Every deployment returns the values you need for a route table: + +| Output | Description | +|---|---| +| `privateIpAddress` | Private IP of the router — use this as the UDR next hop | +| `publicIpAddress` | Public IP, empty when `deployPublicIpAddress` is `false` | +| `subnetId` | Resource ID of the subnet the router joined | + +--- + +## Linux router + +Deploys an Ubuntu router with a single NIC and IP forwarding enabled. The setup script enables IPv4 and IPv6 forwarding, disables ICMP redirects, configures `iptables` SNAT (masquerade) to the internet for private-range sources, and persists all of it across reboots with `netfilter-persistent`. @@ -112,10 +136,11 @@ Deploys an Ubuntu router with a single NIC and IP forwarding enabled. The setup | `virtualMachineName` | string | *(required)* | Name of the router VM. | | `adminUsername` | string | *(required)* | Local admin user name. | | `adminPassword` | secure string | *(required)* | Local admin password. | -| `existingVirtualNetworkName` | string | *(required)* | Name of the existing VNET. | -| `existingSubnet` | string | *(required)* | Existing subnet name. **`linux-router` only.** | -| `subnetName` | string | `lxnva-subnet` | Subnet to create. **`linux-router-newsubnet` only.** | -| `subnetPrefix` | string | *(required)* | CIDR for the new subnet, can be as small as /29. **`linux-router-newsubnet` only.** | +| `networkMode` | string | `ExistingSubnet` | `ExistingSubnet`, `NewSubnet` or `NewVnet` — see [Network modes](#network-modes). | +| `virtualNetworkName` | string | *(required)* | VNET to join, or to create in `NewVnet` mode. | +| `virtualNetworkAddressPrefix` | string | `10.100.0.0/16` | Address space for the new VNET. `NewVnet` only. | +| `subnetName` | string | *(required)* | Subnet to join, or to create in `NewSubnet` / `NewVnet` mode. | +| `subnetAddressPrefix` | string | `10.100.0.0/24` | CIDR for the new subnet, can be as small as /29. `NewSubnet` / `NewVnet` only. | | `osVersion` | string | `24.04` | Ubuntu LTS version — `24.04` or `22.04`. | | `virtualMachineSize` | string | `Standard_B2s` | VM size. | | `osDiskType` | string | `Standard_LRS` | `Premium_LRS`, `StandardSSD_LRS` or `Standard_LRS`. | @@ -143,8 +168,11 @@ Deploys a **Windows Server Core, small disk, Generation 2** router with Trusted | `virtualMachineName` | string | *(required)* | Name of the router VM. | | `adminUsername` | string | *(required)* | Local admin user name. | | `adminPassword` | secure string | *(required)* | Local admin password. | -| `existingVirtualNetworkName` | string | *(required)* | Name of the existing VNET. | -| `existingSubnet` | string | *(required)* | Existing subnet name. | +| `networkMode` | string | `ExistingSubnet` | `ExistingSubnet`, `NewSubnet` or `NewVnet` — see [Network modes](#network-modes). | +| `virtualNetworkName` | string | *(required)* | VNET to join, or to create in `NewVnet` mode. | +| `virtualNetworkAddressPrefix` | string | `10.100.0.0/16` | Address space for the new VNET. `NewVnet` only. | +| `subnetName` | string | *(required)* | Subnet to join, or to create in `NewSubnet` / `NewVnet` mode. | +| `subnetAddressPrefix` | string | `10.100.0.0/24` | CIDR for the new subnet, can be as small as /29. `NewSubnet` / `NewVnet` only. | | `osVersion` | string | `2025` | Windows Server version — `2025`, `2022` or `2019`. | | `virtualMachineSize` | string | `Standard_B2s` | VM size. | | `osDiskType` | string | `Standard_LRS` | `Premium_LRS`, `StandardSSD_LRS` or `Standard_LRS`. | @@ -181,7 +209,7 @@ Deploying the VM does not by itself send any traffic through it. To route traffi ## Deploying from the command line -Deploy the generated ARM template directly from GitHub: +Deploy the generated ARM template directly from GitHub. Joining an existing subnet: ```bash az group create -n rg-nva -l eastus @@ -193,13 +221,34 @@ az deployment group create \ virtualMachineName=nva1 \ adminUsername=azureuser \ adminPassword='' \ - existingVirtualNetworkName=vnet1 \ - existingSubnet=nva-subnet \ + networkMode=ExistingSubnet \ + virtualNetworkName=vnet1 \ + subnetName=nva-subnet \ + allowSshFromAddressPrefix="$(curl -s ifconfig.me)/32" +``` + +Building the VNET and subnet from scratch: + +```bash +az deployment group create \ + -g rg-nva \ + --template-uri https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/infra/arm/linux-router.json \ + --parameters \ + virtualMachineName=nva1 \ + adminUsername=azureuser \ + adminPassword='' \ + networkMode=NewVnet \ + virtualNetworkName=vnet-nva \ + virtualNetworkAddressPrefix=10.100.0.0/16 \ + subnetName=lxnva-subnet \ + subnetAddressPrefix=10.100.0.0/24 \ allowSshFromAddressPrefix="$(curl -s ifconfig.me)/32" ``` +Use `networkMode=NewSubnet` to add the subnet to a VNET that already exists. + > [!WARNING] -> `scriptUri` defaults to a path resolved **relative to the template's own URL**, so it automatically follows the branch or fork you deploy from. That resolution relies on `deployment().properties.templateLink`, which does **not** exist when you deploy a local file with `--template-file`. In that case pass the script location explicitly: +> `scriptUri` defaults to a path resolved **relative to the template's own URL**, so it automatically follows the branch or fork you deploy from. That resolution relies on `deployment().properties.templateLink`, which is not populated when you deploy a local file with `--template-file` or from a template spec. In those cases the default falls back to the `master` branch on GitHub. Pass the script location explicitly to pin it elsewhere: > > ```bash > --parameters scriptUri=https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/scripts/linux/linuxrouter.sh @@ -207,6 +256,35 @@ az deployment group create \ --- +## Publishing as a template spec + +[Template specs](https://learn.microsoft.com/azure/azure-resource-manager/templates/template-specs) let you store a versioned template in your own subscription and share it through Azure RBAC, so consumers deploy it without needing access to this repository. `tools/publish-templatespecs.sh` publishes both routers: + +```bash +./tools/publish-templatespecs.sh rg-templatespecs 1.0.0 eastus +``` + +Then deploy from the spec: + +```bash +az deployment group create \ + -g rg-nva \ + --template-spec "$(az ts show -g rg-templatespecs -n linux-router --version 1.0.0 --query id -o tsv)" \ + --parameters \ + virtualMachineName=nva1 \ + adminUsername=azureuser \ + adminPassword='' \ + networkMode=NewVnet \ + virtualNetworkName=vnet-nva \ + subnetName=lxnva-subnet \ + allowSshFromAddressPrefix="$(curl -s ifconfig.me)/32" +``` + +> [!NOTE] +> A template spec deployment does not expose the original template URL, so `scriptUri` falls back to the `master` branch of this repository. Pass `scriptUri` explicitly if you host the setup scripts somewhere else. + +--- + ## Setup scripts Custom Script Extension payloads under `scripts/`. @@ -247,9 +325,8 @@ End-to-end environment builds under `labs/`, intended to be run interactively li Rebuild the ARM JSON after changing any Bicep file: ```bash -az bicep build --file infra/bicep/linux-router.bicep --outfile infra/arm/linux-router.json -az bicep build --file infra/bicep/linux-router-newsubnet.bicep --outfile infra/arm/linux-router-newsubnet.json -az bicep build --file infra/bicep/windows-router.bicep --outfile infra/arm/windows-router.json +az bicep build --file infra/bicep/linux-router.bicep --outfile infra/arm/linux-router.json +az bicep build --file infra/bicep/windows-router.bicep --outfile infra/arm/windows-router.json ``` CI runs `bicep lint`, rebuilds every template and fails if `infra/arm/` differs from the committed output. It also checks the shell scripts for syntax errors and rejects CRLF line endings, which break the shebang when the Custom Script Extension runs a script on Linux. @@ -267,9 +344,11 @@ The templates and scripts were modernised after several Azure platform retiremen | **Public IP** | Basic SKU (retired September 2025) replaced with **Standard SKU, static allocation** across all templates and lab scripts. | | **Network security** | Added `allowSshFromAddressPrefix` / `allowRdpFromAddressPrefix` so the templates can create the NSG that Standard SKU public IPs now require, together with an RFC 1918 rule so forwarded traffic still flows. Lab scripts that previously created no NSG now create one. | | **Provisioning reliability** | Fixed a latent **cloud-init race** that intermittently failed package installation with `Unable to locate package netfilter-persistent`. All package-installing scripts now wait for cloud-init to finish first. | -| **Repository layout** | Reorganised into `infra/`, `scripts/`, `labs/` and `docs/`, with consistent file naming. | +| **Repository layout** | Reorganised into `infra/`, `scripts/`, `labs/`, `tools/` and `docs/`, with consistent file naming. | +| **Consolidated network modes** | The separate "existing subnet" and "new subnet" templates were merged into one template per OS. A `networkMode` parameter now selects **`ExistingSubnet`**, **`NewSubnet`** or **`NewVnet`**, and the capability was extended to Windows, which previously only supported an existing subnet. Templates now also emit `privateIpAddress`, `publicIpAddress` and `subnetId` outputs. | +| **Template specs** | Added `tools/publish-templatespecs.sh` and made `scriptUri` resolve safely when `deployment().properties.templateLink` is unavailable, so the templates work identically from a URL, a local file or a template spec. | | **Quality gates** | Added GitHub Actions validation and a `.gitattributes` that pins shell scripts to LF. | -| **Documentation** | Rewrote this README with parameter references, template comparisons, security guidance and coverage of every script in the repository. | +| **Documentation** | Rewrote this README with parameter references, network mode guidance, security guidance and coverage of every script in the repository. | All templates and the affected lab scripts were verified by deploying them to Azure and confirming NSG placement, inbound reachability, extension success, in-guest forwarding and NAT state, end-to-end egress through the NVA, and persistence across a reboot. diff --git a/infra/arm/linux-router-newsubnet.json b/infra/arm/linux-router-newsubnet.json deleted file mode 100644 index db43b74..0000000 --- a/infra/arm/linux-router-newsubnet.json +++ /dev/null @@ -1,259 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.46.1.21595", - "templateHash": "2884941074303245104" - } - }, - "parameters": { - "virtualMachineSize": { - "type": "string", - "defaultValue": "Standard_B2s", - "metadata": { - "description": "VM size" - } - }, - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Linux Router Machine Name" - } - }, - "osDiskType": { - "type": "string", - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "StandardSSD_LRS", - "Premium_LRS" - ], - "metadata": { - "description": "Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)" - } - }, - "osVersion": { - "type": "string", - "defaultValue": "24.04", - "allowedValues": [ - "22.04", - "24.04" - ], - "metadata": { - "description": "Ubuntu OS Version" - } - }, - "adminUsername": { - "type": "string", - "metadata": { - "description": "Admin username" - } - }, - "adminPassword": { - "type": "securestring", - "metadata": { - "description": "Admin password" - } - }, - "existingVirtualNetworkName": { - "type": "string", - "metadata": { - "description": "Existing Virtual Network Name" - } - }, - "subnetName": { - "type": "string", - "defaultValue": "lxnva-subnet", - "metadata": { - "description": "Name of the Subnet where NVA will reside" - } - }, - "subnetPrefix": { - "type": "string", - "metadata": { - "description": "Specify Subnet Prefix. It can be small as /29" - } - }, - "scriptUri": { - "type": "string", - "defaultValue": "[uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh')]", - "metadata": { - "description": "Script that will be executed" - } - }, - "scriptCmd": { - "type": "string", - "defaultValue": "sh linuxrouter.sh", - "metadata": { - "description": "Command to run the script" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region for all resources." - } - }, - "deployPublicIpAddress": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy Public IP Address" - } - }, - "allowSshFromAddressPrefix": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended)." - } - } - }, - "variables": { - "extensionName": "CustomScript", - "nicName": "[format('{0}-NIC', parameters('virtualMachineName'))]", - "publicIPAddressName": "[format('{0}-PublicIP', parameters('virtualMachineName'))]", - "sshSecurityRules": "[if(empty(parameters('allowSshFromAddressPrefix')), createArray(), createArray(createObject('name', 'Allow-SSH-Inbound', 'properties', createObject('priority', 200, 'protocol', 'Tcp', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefix', parameters('allowSshFromAddressPrefix'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '22'))))]", - "osVersionDefinitions": { - "22.04": { - "publisher": "Canonical", - "offer": "0001-com-ubuntu-server-jammy", - "sku": "22_04-lts-gen2", - "version": "latest" - }, - "24.04": { - "publisher": "Canonical", - "offer": "ubuntu-24_04-lts", - "sku": "server", - "version": "latest" - } - } - }, - "resources": [ - { - "type": "Microsoft.Network/networkSecurityGroups", - "apiVersion": "2024-05-01", - "name": "default-nsg", - "location": "[parameters('location')]", - "properties": { - "securityRules": "[concat(variables('sshSecurityRules'), createArray(createObject('name', 'Allow-Traffic-RFC-1918', 'properties', createObject('priority', 300, 'protocol', '*', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefixes', createArray('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '*'))))]" - } - }, - { - "type": "Microsoft.Network/virtualNetworks/subnets", - "apiVersion": "2024-05-01", - "name": "[format('{0}/{1}', parameters('existingVirtualNetworkName'), parameters('subnetName'))]", - "properties": { - "addressPrefix": "[parameters('subnetPrefix')]", - "networkSecurityGroup": { - "id": "[resourceId('Microsoft.Network/networkSecurityGroups', 'default-nsg')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/networkSecurityGroups', 'default-nsg')]" - ] - }, - { - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-07-01", - "name": "[parameters('virtualMachineName')]", - "location": "[parameters('location')]", - "properties": { - "osProfile": { - "computerName": "[parameters('virtualMachineName')]", - "adminUsername": "[parameters('adminUsername')]", - "adminPassword": "[parameters('adminPassword')]" - }, - "hardwareProfile": { - "vmSize": "[parameters('virtualMachineSize')]" - }, - "storageProfile": { - "imageReference": "[variables('osVersionDefinitions')[parameters('osVersion')]]", - "osDisk": { - "createOption": "FromImage", - "name": "[format('{0}-OSDisk', parameters('virtualMachineName'))]", - "managedDisk": { - "storageAccountType": "[parameters('osDiskType')]" - } - }, - "dataDisks": [] - }, - "networkProfile": { - "networkInterfaces": [ - { - "properties": { - "primary": true - }, - "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" - ] - }, - { - "type": "Microsoft.Network/networkInterfaces", - "apiVersion": "2024-05-01", - "name": "[variables('nicName')]", - "location": "[parameters('location')]", - "properties": { - "enableIPForwarding": true, - "ipConfigurations": [ - { - "name": "ipconfig1", - "properties": { - "subnet": { - "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('subnetName'))]" - }, - "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": "[if(parameters('deployPublicIpAddress'), createObject('id', resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))), null())]" - } - } - ] - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]", - "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('subnetName'))]" - ] - }, - { - "condition": "[parameters('deployPublicIpAddress')]", - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2024-05-01", - "name": "[variables('publicIPAddressName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Standard" - }, - "properties": { - "publicIPAllocationMethod": "Static" - } - }, - { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-07-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), variables('extensionName'))]", - "location": "[parameters('location')]", - "properties": { - "publisher": "Microsoft.Azure.Extensions", - "type": "CustomScript", - "typeHandlerVersion": "2.0", - "autoUpgradeMinorVersion": true, - "settings": { - "fileUris": [ - "[parameters('scriptUri')]" - ], - "commandToExecute": "[parameters('scriptCmd')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/infra/arm/linux-router.json b/infra/arm/linux-router.json index ee7149f..0e7ac93 100644 --- a/infra/arm/linux-router.json +++ b/infra/arm/linux-router.json @@ -5,10 +5,16 @@ "_generator": { "name": "bicep", "version": "0.46.1.21595", - "templateHash": "8873189144452211355" + "templateHash": "11051965274704034192" } }, "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Linux Router Machine Name" + } + }, "virtualMachineSize": { "type": "string", "defaultValue": "Standard_B2s", @@ -16,10 +22,15 @@ "description": "VM size" } }, - "virtualMachineName": { + "osVersion": { "type": "string", + "defaultValue": "24.04", + "allowedValues": [ + "22.04", + "24.04" + ], "metadata": { - "description": "Linux Router Machine Name" + "description": "Ubuntu OS Version" } }, "osDiskType": { @@ -34,17 +45,6 @@ "description": "Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)" } }, - "osVersion": { - "type": "string", - "defaultValue": "24.04", - "allowedValues": [ - "22.04", - "24.04" - ], - "metadata": { - "description": "Ubuntu OS Version" - } - }, "adminUsername": { "type": "string", "metadata": { @@ -57,37 +57,42 @@ "description": "Admin password" } }, - "existingVirtualNetworkName": { + "networkMode": { "type": "string", + "defaultValue": "ExistingSubnet", + "allowedValues": [ + "ExistingSubnet", + "NewSubnet", + "NewVnet" + ], "metadata": { - "description": "Existing Virtual Network Name" + "description": "How the router is attached to the network. ExistingSubnet: join a virtual network and subnet that already exist. NewSubnet: join an existing virtual network and create a new subnet in it. NewVnet: create a new virtual network and subnet." } }, - "existingSubnet": { + "virtualNetworkName": { "type": "string", "metadata": { - "description": "Type Existing Subnet Name" + "description": "Virtual network name. Must already exist for ExistingSubnet and NewSubnet; created for NewVnet." } }, - "scriptUri": { + "virtualNetworkAddressPrefix": { "type": "string", - "defaultValue": "[uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh')]", + "defaultValue": "10.100.0.0/16", "metadata": { - "description": "Script that will be executed" + "description": "Address space for the virtual network. Only used when networkMode is NewVnet." } }, - "scriptCmd": { + "subnetName": { "type": "string", - "defaultValue": "sh linuxrouter.sh", "metadata": { - "description": "Command to run the script" + "description": "Subnet name. Must already exist for ExistingSubnet; created for NewSubnet and NewVnet." } }, - "location": { + "subnetAddressPrefix": { "type": "string", - "defaultValue": "[resourceGroup().location]", + "defaultValue": "10.100.0.0/24", "metadata": { - "description": "Azure region for all resources." + "description": "CIDR for the subnet to create, can be as small as /29. Only used when networkMode is NewSubnet or NewVnet." } }, "deployPublicIpAddress": { @@ -103,6 +108,27 @@ "metadata": { "description": "Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended)." } + }, + "scriptUri": { + "type": "string", + "defaultValue": "[uri(coalesce(tryGet(tryGet(deployment().properties, 'templateLink'), 'uri'), 'https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/infra/arm/linux-router.json'), '../../scripts/linux/linuxrouter.sh')]", + "metadata": { + "description": "Script that will be executed. Defaults to the script alongside this template when deployed from a URL, and to the master branch on GitHub when deployed from a local file or a template spec." + } + }, + "scriptCmd": { + "type": "string", + "defaultValue": "sh linuxrouter.sh", + "metadata": { + "description": "Command to run the script" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region for all resources." + } } }, "variables": { @@ -110,8 +136,13 @@ "nicName": "[format('{0}-NIC', parameters('virtualMachineName'))]", "nsgName": "[format('{0}-NSG', parameters('virtualMachineName'))]", "publicIPAddressName": "[format('{0}-PublicIP', parameters('virtualMachineName'))]", - "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('existingSubnet'))]", - "deployNetworkSecurityGroup": "[not(empty(parameters('allowSshFromAddressPrefix')))]", + "createVirtualNetwork": "[equals(parameters('networkMode'), 'NewVnet')]", + "createSubnet": "[not(equals(parameters('networkMode'), 'ExistingSubnet'))]", + "attachNsgToSubnet": "[variables('createSubnet')]", + "attachNsgToNic": "[and(not(variables('createSubnet')), not(empty(parameters('allowSshFromAddressPrefix'))))]", + "deployNetworkSecurityGroup": "[or(variables('attachNsgToSubnet'), variables('attachNsgToNic'))]", + "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]", + "sshSecurityRules": "[if(empty(parameters('allowSshFromAddressPrefix')), createArray(), createArray(createObject('name', 'Allow-SSH-Inbound', 'properties', createObject('priority', 200, 'protocol', 'Tcp', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefix', parameters('allowSshFromAddressPrefix'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '22'))))]", "osVersionDefinitions": { "22.04": { "publisher": "Canonical", @@ -129,84 +160,69 @@ }, "resources": [ { - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-07-01", - "name": "[parameters('virtualMachineName')]", + "condition": "[variables('deployNetworkSecurityGroup')]", + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2024-05-01", + "name": "[variables('nsgName')]", "location": "[parameters('location')]", "properties": { - "osProfile": { - "computerName": "[parameters('virtualMachineName')]", - "adminUsername": "[parameters('adminUsername')]", - "adminPassword": "[parameters('adminPassword')]" - }, - "hardwareProfile": { - "vmSize": "[parameters('virtualMachineSize')]" - }, - "storageProfile": { - "imageReference": "[variables('osVersionDefinitions')[parameters('osVersion')]]", - "osDisk": { - "createOption": "FromImage", - "name": "[format('{0}-OSDisk', parameters('virtualMachineName'))]", - "managedDisk": { - "storageAccountType": "[parameters('osDiskType')]" - } - }, - "dataDisks": [] + "securityRules": "[concat(variables('sshSecurityRules'), createArray(createObject('name', 'Allow-Traffic-RFC-1918', 'properties', createObject('priority', 300, 'protocol', '*', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefixes', createArray('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '*'))))]" + } + }, + { + "condition": "[variables('createVirtualNetwork')]", + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2024-05-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('virtualNetworkAddressPrefix')]" + ] }, - "networkProfile": { - "networkInterfaces": [ - { - "properties": { - "primary": true - }, - "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + "subnets": [ + { + "name": "[parameters('subnetName')]", + "properties": { + "addressPrefix": "[parameters('subnetAddressPrefix')]", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + } } - ] + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + ] + }, + { + "condition": "[equals(parameters('networkMode'), 'NewSubnet')]", + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('virtualNetworkName'), parameters('subnetName'))]", + "properties": { + "addressPrefix": "[parameters('subnetAddressPrefix')]", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" } }, "dependsOn": [ - "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" ] }, { - "condition": "[variables('deployNetworkSecurityGroup')]", - "type": "Microsoft.Network/networkSecurityGroups", + "condition": "[parameters('deployPublicIpAddress')]", + "type": "Microsoft.Network/publicIPAddresses", "apiVersion": "2024-05-01", - "name": "[variables('nsgName')]", + "name": "[variables('publicIPAddressName')]", "location": "[parameters('location')]", + "sku": { + "name": "Standard" + }, "properties": { - "securityRules": [ - { - "name": "Allow-SSH-Inbound", - "properties": { - "priority": 200, - "protocol": "Tcp", - "access": "Allow", - "direction": "Inbound", - "sourceAddressPrefix": "[parameters('allowSshFromAddressPrefix')]", - "sourcePortRange": "*", - "destinationAddressPrefix": "*", - "destinationPortRange": "22" - } - }, - { - "name": "Allow-Traffic-RFC-1918", - "properties": { - "priority": 300, - "protocol": "*", - "access": "Allow", - "direction": "Inbound", - "sourceAddressPrefixes": [ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ], - "sourcePortRange": "*", - "destinationAddressPrefix": "*", - "destinationPortRange": "*" - } - } - ] + "publicIPAllocationMethod": "Static" } }, { @@ -216,7 +232,7 @@ "location": "[parameters('location')]", "properties": { "enableIPForwarding": true, - "networkSecurityGroup": "[if(variables('deployNetworkSecurityGroup'), createObject('id', resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))), null())]", + "networkSecurityGroup": "[if(variables('attachNsgToNic'), createObject('id', resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))), null())]", "ipConfigurations": [ { "name": "ipconfig1", @@ -231,22 +247,51 @@ ] }, "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]", "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]", + "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" ] }, { - "condition": "[parameters('deployPublicIpAddress')]", - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2024-05-01", - "name": "[variables('publicIPAddressName')]", + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-07-01", + "name": "[parameters('virtualMachineName')]", "location": "[parameters('location')]", - "sku": { - "name": "Standard" - }, "properties": { - "publicIPAllocationMethod": "Static" - } + "osProfile": { + "computerName": "[parameters('virtualMachineName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]" + }, + "hardwareProfile": { + "vmSize": "[parameters('virtualMachineSize')]" + }, + "storageProfile": { + "imageReference": "[variables('osVersionDefinitions')[parameters('osVersion')]]", + "osDisk": { + "createOption": "FromImage", + "name": "[format('{0}-OSDisk', parameters('virtualMachineName'))]", + "managedDisk": { + "storageAccountType": "[parameters('osDiskType')]" + } + }, + "dataDisks": [] + }, + "networkProfile": { + "networkInterfaces": [ + { + "properties": { + "primary": true + }, + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + ] }, { "type": "Microsoft.Compute/virtualMachines/extensions", @@ -269,5 +314,28 @@ "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]" ] } - ] + ], + "outputs": { + "privateIpAddress": { + "type": "string", + "metadata": { + "description": "Private IP address of the router, use this as the next hop in a route table." + }, + "value": "[reference(resourceId('Microsoft.Network/networkInterfaces', variables('nicName')), '2024-05-01').ipConfigurations[0].properties.privateIPAddress]" + }, + "publicIpAddress": { + "type": "string", + "metadata": { + "description": "Public IP address of the router, empty when deployPublicIpAddress is false." + }, + "value": "[coalesce(tryGet(if(parameters('deployPublicIpAddress'), reference(resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName')), '2024-05-01', 'full'), null()), 'properties', 'ipAddress'), '')]" + }, + "subnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the subnet the router is attached to." + }, + "value": "[variables('subnetResourceId')]" + } + } } \ No newline at end of file diff --git a/infra/arm/windows-router.json b/infra/arm/windows-router.json index d201878..c0a28c8 100644 --- a/infra/arm/windows-router.json +++ b/infra/arm/windows-router.json @@ -5,10 +5,16 @@ "_generator": { "name": "bicep", "version": "0.46.1.21595", - "templateHash": "8395921165082925479" + "templateHash": "18243471132502850443" } }, "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Windows Router Machine Name" + } + }, "virtualMachineSize": { "type": "string", "defaultValue": "Standard_B2s", @@ -16,10 +22,16 @@ "description": "VM size" } }, - "virtualMachineName": { + "osVersion": { "type": "string", + "defaultValue": "2025", + "allowedValues": [ + "2019", + "2022", + "2025" + ], "metadata": { - "description": "Windows Router Machine Name" + "description": "Windows Server version. All options are Server Core, small disk, Generation 2 images." } }, "osDiskType": { @@ -34,18 +46,6 @@ "description": "Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)" } }, - "osVersion": { - "type": "string", - "defaultValue": "2025", - "allowedValues": [ - "2019", - "2022", - "2025" - ], - "metadata": { - "description": "Windows Server version. All options are Server Core, small disk, Generation 2 images." - } - }, "adminUsername": { "type": "string", "metadata": { @@ -58,37 +58,42 @@ "description": "Admin password" } }, - "existingVirtualNetworkName": { + "networkMode": { "type": "string", + "defaultValue": "ExistingSubnet", + "allowedValues": [ + "ExistingSubnet", + "NewSubnet", + "NewVnet" + ], "metadata": { - "description": "Existing Virtual Network Name" + "description": "How the router is attached to the network. ExistingSubnet: join a virtual network and subnet that already exist. NewSubnet: join an existing virtual network and create a new subnet in it. NewVnet: create a new virtual network and subnet." } }, - "existingSubnet": { + "virtualNetworkName": { "type": "string", "metadata": { - "description": "Type Existing Subnet Name" + "description": "Virtual network name. Must already exist for ExistingSubnet and NewSubnet; created for NewVnet." } }, - "scriptUri": { + "virtualNetworkAddressPrefix": { "type": "string", - "defaultValue": "[uri(deployment().properties.templateLink.uri, '../../scripts/windows/winrouter.ps1')]", + "defaultValue": "10.100.0.0/16", "metadata": { - "description": "Script that will be executed" + "description": "Address space for the virtual network. Only used when networkMode is NewVnet." } }, - "scriptCmd": { + "subnetName": { "type": "string", - "defaultValue": "powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1", "metadata": { - "description": "Command to run the script" + "description": "Subnet name. Must already exist for ExistingSubnet; created for NewSubnet and NewVnet." } }, - "location": { + "subnetAddressPrefix": { "type": "string", - "defaultValue": "[resourceGroup().location]", + "defaultValue": "10.100.0.0/24", "metadata": { - "description": "Azure region for all resources." + "description": "CIDR for the subnet to create, can be as small as /29. Only used when networkMode is NewSubnet or NewVnet." } }, "deployPublicIpAddress": { @@ -104,6 +109,27 @@ "metadata": { "description": "Source address prefix allowed to reach the VM on TCP 3389, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need RDP from the internet. Use Internet to allow any source (not recommended)." } + }, + "scriptUri": { + "type": "string", + "defaultValue": "[uri(coalesce(tryGet(tryGet(deployment().properties, 'templateLink'), 'uri'), 'https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/infra/arm/windows-router.json'), '../../scripts/windows/winrouter.ps1')]", + "metadata": { + "description": "Script that will be executed. Defaults to the script alongside this template when deployed from a URL, and to the master branch on GitHub when deployed from a local file or a template spec." + } + }, + "scriptCmd": { + "type": "string", + "defaultValue": "powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1", + "metadata": { + "description": "Command to run the script" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region for all resources." + } } }, "variables": { @@ -111,8 +137,13 @@ "nicName": "[format('{0}-NIC', parameters('virtualMachineName'))]", "nsgName": "[format('{0}-NSG', parameters('virtualMachineName'))]", "publicIPAddressName": "[format('{0}-PublicIP', parameters('virtualMachineName'))]", - "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('existingVirtualNetworkName'), parameters('existingSubnet'))]", - "deployNetworkSecurityGroup": "[not(empty(parameters('allowRdpFromAddressPrefix')))]", + "createVirtualNetwork": "[equals(parameters('networkMode'), 'NewVnet')]", + "createSubnet": "[not(equals(parameters('networkMode'), 'ExistingSubnet'))]", + "attachNsgToSubnet": "[variables('createSubnet')]", + "attachNsgToNic": "[and(not(variables('createSubnet')), not(empty(parameters('allowRdpFromAddressPrefix'))))]", + "deployNetworkSecurityGroup": "[or(variables('attachNsgToSubnet'), variables('attachNsgToNic'))]", + "subnetResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]", + "rdpSecurityRules": "[if(empty(parameters('allowRdpFromAddressPrefix')), createArray(), createArray(createObject('name', 'Allow-RDP-Inbound', 'properties', createObject('priority', 200, 'protocol', 'Tcp', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefix', parameters('allowRdpFromAddressPrefix'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '3389'))))]", "osVersionDefinitions": { "2019": { "publisher": "MicrosoftWindowsServer", @@ -142,39 +173,92 @@ "name": "[variables('nsgName')]", "location": "[parameters('location')]", "properties": { - "securityRules": [ + "securityRules": "[concat(variables('rdpSecurityRules'), createArray(createObject('name', 'Allow-Traffic-RFC-1918', 'properties', createObject('priority', 300, 'protocol', '*', 'access', 'Allow', 'direction', 'Inbound', 'sourceAddressPrefixes', createArray('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'), 'sourcePortRange', '*', 'destinationAddressPrefix', '*', 'destinationPortRange', '*'))))]" + } + }, + { + "condition": "[variables('createVirtualNetwork')]", + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2024-05-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('virtualNetworkAddressPrefix')]" + ] + }, + "subnets": [ { - "name": "Allow-RDP-Inbound", + "name": "[parameters('subnetName')]", "properties": { - "priority": 200, - "protocol": "Tcp", - "access": "Allow", - "direction": "Inbound", - "sourceAddressPrefix": "[parameters('allowRdpFromAddressPrefix')]", - "sourcePortRange": "*", - "destinationAddressPrefix": "*", - "destinationPortRange": "3389" + "addressPrefix": "[parameters('subnetAddressPrefix')]", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + } } - }, + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + ] + }, + { + "condition": "[equals(parameters('networkMode'), 'NewSubnet')]", + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('virtualNetworkName'), parameters('subnetName'))]", + "properties": { + "addressPrefix": "[parameters('subnetAddressPrefix')]", + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + ] + }, + { + "condition": "[parameters('deployPublicIpAddress')]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "name": "[variables('publicIPAddressName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard" + }, + "properties": { + "publicIPAllocationMethod": "Static" + } + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2024-05-01", + "name": "[variables('nicName')]", + "location": "[parameters('location')]", + "properties": { + "enableIPForwarding": true, + "networkSecurityGroup": "[if(variables('attachNsgToNic'), createObject('id', resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))), null())]", + "ipConfigurations": [ { - "name": "Allow-Traffic-RFC-1918", + "name": "ipconfig1", "properties": { - "priority": 300, - "protocol": "*", - "access": "Allow", - "direction": "Inbound", - "sourceAddressPrefixes": [ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ], - "sourcePortRange": "*", - "destinationAddressPrefix": "*", - "destinationPortRange": "*" + "subnet": { + "id": "[variables('subnetResourceId')]" + }, + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": "[if(parameters('deployPublicIpAddress'), createObject('id', resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))), null())]" } } ] - } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]", + "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]", + "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", + "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" + ] }, { "type": "Microsoft.Compute/virtualMachines", @@ -223,45 +307,6 @@ "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" ] }, - { - "type": "Microsoft.Network/networkInterfaces", - "apiVersion": "2024-05-01", - "name": "[variables('nicName')]", - "location": "[parameters('location')]", - "properties": { - "enableIPForwarding": true, - "networkSecurityGroup": "[if(variables('deployNetworkSecurityGroup'), createObject('id', resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))), null())]", - "ipConfigurations": [ - { - "name": "ipconfig1", - "properties": { - "subnet": { - "id": "[variables('subnetResourceId')]" - }, - "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": "[if(parameters('deployPublicIpAddress'), createObject('id', resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))), null())]" - } - } - ] - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]", - "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" - ] - }, - { - "condition": "[parameters('deployPublicIpAddress')]", - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2024-05-01", - "name": "[variables('publicIPAddressName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Standard" - }, - "properties": { - "publicIPAllocationMethod": "Static" - } - }, { "type": "Microsoft.Compute/virtualMachines/extensions", "apiVersion": "2024-07-01", @@ -283,5 +328,28 @@ "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]" ] } - ] + ], + "outputs": { + "privateIpAddress": { + "type": "string", + "metadata": { + "description": "Private IP address of the router, use this as the next hop in a route table." + }, + "value": "[reference(resourceId('Microsoft.Network/networkInterfaces', variables('nicName')), '2024-05-01').ipConfigurations[0].properties.privateIPAddress]" + }, + "publicIpAddress": { + "type": "string", + "metadata": { + "description": "Public IP address of the router, empty when deployPublicIpAddress is false." + }, + "value": "[coalesce(tryGet(if(parameters('deployPublicIpAddress'), reference(resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName')), '2024-05-01', 'full'), null()), 'properties', 'ipAddress'), '')]" + }, + "subnetId": { + "type": "string", + "metadata": { + "description": "Resource ID of the subnet the router is attached to." + }, + "value": "[variables('subnetResourceId')]" + } + } } \ No newline at end of file diff --git a/infra/bicep/linux-router-newsubnet.bicep b/infra/bicep/linux-router-newsubnet.bicep deleted file mode 100644 index 5a4d2ca..0000000 --- a/infra/bicep/linux-router-newsubnet.bicep +++ /dev/null @@ -1,211 +0,0 @@ -@description('VM size') -param virtualMachineSize string = 'Standard_B2s' - -@description('Linux Router Machine Name') -param virtualMachineName string - -@description('Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)') -@allowed([ - 'Standard_LRS' - 'StandardSSD_LRS' - 'Premium_LRS' -]) -param osDiskType string = 'Standard_LRS' - -@description('Ubuntu OS Version') -@allowed(['22.04', '24.04']) -param osVersion string = '24.04' - -@description('Admin username') -param adminUsername string - -@description('Admin password') -@secure() -param adminPassword string - -@description('Existing Virtual Network Name') -param existingVirtualNetworkName string - -@description('Name of the Subnet where NVA will reside') -param subnetName string = 'lxnva-subnet' - -@description('Specify Subnet Prefix. It can be small as /29') -param subnetPrefix string - -@description('Script that will be executed') -param scriptUri string = uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh') - -@description('Command to run the script') -param scriptCmd string = 'sh linuxrouter.sh' - -@description('Azure region for all resources.') -param location string = resourceGroup().location - -@description('Deploy Public IP Address') -param deployPublicIpAddress bool = true - -@description('Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended).') -param allowSshFromAddressPrefix string = '' - -var extensionName = 'CustomScript' -var nicName = '${virtualMachineName}-NIC' -var publicIPAddressName = '${virtualMachineName}-PublicIP' - -var sshSecurityRules = empty(allowSshFromAddressPrefix) ? [] : [ - { - name: 'Allow-SSH-Inbound' - properties: { - priority: 200 - protocol: 'Tcp' - access: 'Allow' - direction: 'Inbound' - sourceAddressPrefix: allowSshFromAddressPrefix - sourcePortRange: '*' - destinationAddressPrefix: '*' - destinationPortRange: '22' - } - } -] - -var osVersionDefinitions = { - '22.04': { - publisher: 'Canonical' - offer: '0001-com-ubuntu-server-jammy' - sku: '22_04-lts-gen2' - version: 'latest' - } - '24.04': { - publisher: 'Canonical' - offer: 'ubuntu-24_04-lts' - sku: 'server' - version: 'latest' - } -} - -resource default_nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { - name: 'default-nsg' - location: location - properties: { - securityRules: concat(sshSecurityRules, [ - { - name: 'Allow-Traffic-RFC-1918' - properties: { - priority: 300 - protocol: '*' - access: 'Allow' - direction: 'Inbound' - sourceAddressPrefixes: [ - '10.0.0.0/8' - '172.16.0.0/12' - '192.168.0.0/16' - ] - sourcePortRange: '*' - destinationAddressPrefix: '*' - destinationPortRange: '*' - } - } - ]) - } -} - -resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { - name: existingVirtualNetworkName -} - -resource subnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { - name: subnetName - parent: virtualNetwork - properties: { - addressPrefix: subnetPrefix - networkSecurityGroup: { - id: default_nsg.id - } - } -} - -resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { - name: virtualMachineName - location: location - properties: { - osProfile: { - computerName: virtualMachineName - adminUsername: adminUsername - adminPassword: adminPassword - } - hardwareProfile: { - vmSize: virtualMachineSize - } - storageProfile: { - imageReference: osVersionDefinitions[osVersion] - osDisk: { - createOption: 'FromImage' - name: '${virtualMachineName}-OSDisk' - managedDisk: { - storageAccountType: osDiskType - } - } - dataDisks: [] - } - networkProfile: { - networkInterfaces: [ - { - properties: { - primary: true - } - id: nic.id - } - ] - } - } -} - -resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { - name: nicName - location: location - properties: { - enableIPForwarding: true - ipConfigurations: [ - { - name: 'ipconfig1' - properties: { - subnet: { - id: subnet.id - } - privateIPAllocationMethod: 'Dynamic' - publicIPAddress: deployPublicIpAddress ? { - id: publicIpAddress.id - } : null - } - } - ] - } -} - -resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployPublicIpAddress) { - name: publicIPAddressName - location: location - sku: { - name: 'Standard' - } - properties: { - publicIPAllocationMethod: 'Static' - } -} - -resource virtualMachineExtension 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = { - name: extensionName - parent: virtualMachine - location: location - properties: { - publisher: 'Microsoft.Azure.Extensions' - type: 'CustomScript' - typeHandlerVersion: '2.0' - autoUpgradeMinorVersion: true - settings: { - fileUris: [ - scriptUri - ] - commandToExecute: scriptCmd - } - } -} diff --git a/infra/bicep/linux-router.bicep b/infra/bicep/linux-router.bicep index 52693fa..39da9b6 100644 --- a/infra/bicep/linux-router.bicep +++ b/infra/bicep/linux-router.bicep @@ -1,8 +1,12 @@ +@description('Linux Router Machine Name') +param virtualMachineName string + @description('VM size') param virtualMachineSize string = 'Standard_B2s' -@description('Linux Router Machine Name') -param virtualMachineName string +@description('Ubuntu OS Version') +@allowed(['22.04', '24.04']) +param osVersion string = '24.04' @description('Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)') @allowed([ @@ -12,10 +16,6 @@ param virtualMachineName string ]) param osDiskType string = 'Standard_LRS' -@description('Ubuntu OS Version') -@allowed(['22.04', '24.04']) -param osVersion string = '24.04' - @description('Admin username') param adminUsername string @@ -23,20 +23,25 @@ param adminUsername string @secure() param adminPassword string -@description('Existing Virtual Network Name') -param existingVirtualNetworkName string +@description('How the router is attached to the network. ExistingSubnet: join a virtual network and subnet that already exist. NewSubnet: join an existing virtual network and create a new subnet in it. NewVnet: create a new virtual network and subnet.') +@allowed([ + 'ExistingSubnet' + 'NewSubnet' + 'NewVnet' +]) +param networkMode string = 'ExistingSubnet' -@description('Type Existing Subnet Name') -param existingSubnet string +@description('Virtual network name. Must already exist for ExistingSubnet and NewSubnet; created for NewVnet.') +param virtualNetworkName string -@description('Script that will be executed') -param scriptUri string = uri(deployment().properties.templateLink.uri, '../../scripts/linux/linuxrouter.sh') +@description('Address space for the virtual network. Only used when networkMode is NewVnet.') +param virtualNetworkAddressPrefix string = '10.100.0.0/16' -@description('Command to run the script') -param scriptCmd string = 'sh linuxrouter.sh' +@description('Subnet name. Must already exist for ExistingSubnet; created for NewSubnet and NewVnet.') +param subnetName string -@description('Azure region for all resources.') -param location string = resourceGroup().location +@description('CIDR for the subnet to create, can be as small as /29. Only used when networkMode is NewSubnet or NewVnet.') +param subnetAddressPrefix string = '10.100.0.0/24' @description('Deploy Public IP Address') param deployPublicIpAddress bool = true @@ -44,12 +49,47 @@ param deployPublicIpAddress bool = true @description('Source address prefix allowed to reach the VM on TCP 22, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need SSH from the internet. Use Internet to allow any source (not recommended).') param allowSshFromAddressPrefix string = '' +@description('Script that will be executed. Defaults to the script alongside this template when deployed from a URL, and to the master branch on GitHub when deployed from a local file or a template spec.') +param scriptUri string = uri(deployment().properties.?templateLink.?uri ?? 'https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/infra/arm/linux-router.json', '../../scripts/linux/linuxrouter.sh') + +@description('Command to run the script') +param scriptCmd string = 'sh linuxrouter.sh' + +@description('Azure region for all resources.') +param location string = resourceGroup().location + var extensionName = 'CustomScript' var nicName = '${virtualMachineName}-NIC' var nsgName = '${virtualMachineName}-NSG' var publicIPAddressName = '${virtualMachineName}-PublicIP' -var subnetResourceId = resourceId('Microsoft.Network/virtualNetworks/subnets', existingVirtualNetworkName, existingSubnet) -var deployNetworkSecurityGroup = !empty(allowSshFromAddressPrefix) + +var createVirtualNetwork = networkMode == 'NewVnet' +var createSubnet = networkMode != 'ExistingSubnet' + +// When the template creates the subnet it owns the subnet NSG. When joining a +// subnet that already exists, the NSG goes on the NIC instead so that any NSG +// already associated with that subnet is left untouched. +var attachNsgToSubnet = createSubnet +var attachNsgToNic = !createSubnet && !empty(allowSshFromAddressPrefix) +var deployNetworkSecurityGroup = attachNsgToSubnet || attachNsgToNic + +var subnetResourceId = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName) + +var sshSecurityRules = empty(allowSshFromAddressPrefix) ? [] : [ + { + name: 'Allow-SSH-Inbound' + properties: { + priority: 200 + protocol: 'Tcp' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: allowSshFromAddressPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '22' + } + } +] var osVersionDefinitions = { '22.04': { @@ -66,60 +106,11 @@ var osVersionDefinitions = { } } -resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { - name: virtualMachineName - location: location - properties: { - osProfile: { - computerName: virtualMachineName - adminUsername: adminUsername - adminPassword: adminPassword - } - hardwareProfile: { - vmSize: virtualMachineSize - } - storageProfile: { - imageReference: osVersionDefinitions[osVersion] - osDisk: { - createOption: 'FromImage' - name: '${virtualMachineName}-OSDisk' - managedDisk: { - storageAccountType: osDiskType - } - } - dataDisks: [] - } - networkProfile: { - networkInterfaces: [ - { - properties: { - primary: true - } - id: nic.id - } - ] - } - } -} - resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-01' = if (deployNetworkSecurityGroup) { name: nsgName location: location properties: { - securityRules: [ - { - name: 'Allow-SSH-Inbound' - properties: { - priority: 200 - protocol: 'Tcp' - access: 'Allow' - direction: 'Inbound' - sourceAddressPrefix: allowSshFromAddressPrefix - sourcePortRange: '*' - destinationAddressPrefix: '*' - destinationPortRange: '22' - } - } + securityRules: concat(sshSecurityRules, [ { name: 'Allow-Traffic-RFC-1918' properties: { @@ -137,16 +128,65 @@ resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-0 destinationPortRange: '*' } } + ]) + } +} + +resource newVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = if (createVirtualNetwork) { + name: virtualNetworkName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + virtualNetworkAddressPrefix + ] + } + subnets: [ + { + name: subnetName + properties: { + addressPrefix: subnetAddressPrefix + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } + } ] } } +resource targetVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: virtualNetworkName +} + +resource addedSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = if (networkMode == 'NewSubnet') { + parent: targetVirtualNetwork + name: subnetName + properties: { + addressPrefix: subnetAddressPrefix + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } +} + +resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployPublicIpAddress) { + name: publicIPAddressName + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { name: nicName location: location properties: { enableIPForwarding: true - networkSecurityGroup: deployNetworkSecurityGroup ? { + networkSecurityGroup: attachNsgToNic ? { id: networkSecurityGroup.id } : null ipConfigurations: [ @@ -164,20 +204,49 @@ resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { } ] } + dependsOn: [ + newVirtualNetwork + addedSubnet + ] } -resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployPublicIpAddress) { - name: publicIPAddressName +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { + name: virtualMachineName location: location - sku: { - name: 'Standard' - } properties: { - publicIPAllocationMethod: 'Static' + osProfile: { + computerName: virtualMachineName + adminUsername: adminUsername + adminPassword: adminPassword + } + hardwareProfile: { + vmSize: virtualMachineSize + } + storageProfile: { + imageReference: osVersionDefinitions[osVersion] + osDisk: { + createOption: 'FromImage' + name: '${virtualMachineName}-OSDisk' + managedDisk: { + storageAccountType: osDiskType + } + } + dataDisks: [] + } + networkProfile: { + networkInterfaces: [ + { + properties: { + primary: true + } + id: nic.id + } + ] + } } } -resource virtualMachineName_extension 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = { +resource virtualMachineExtension 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = { parent: virtualMachine name: extensionName location: location @@ -194,3 +263,12 @@ resource virtualMachineName_extension 'Microsoft.Compute/virtualMachines/extensi } } } + +@description('Private IP address of the router, use this as the next hop in a route table.') +output privateIpAddress string = nic.properties.ipConfigurations[0].properties.privateIPAddress + +@description('Public IP address of the router, empty when deployPublicIpAddress is false.') +output publicIpAddress string = publicIpAddress.?properties.ipAddress ?? '' + +@description('Resource ID of the subnet the router is attached to.') +output subnetId string = subnetResourceId diff --git a/infra/bicep/windows-router.bicep b/infra/bicep/windows-router.bicep index 648e428..79fbb7b 100644 --- a/infra/bicep/windows-router.bicep +++ b/infra/bicep/windows-router.bicep @@ -1,8 +1,12 @@ +@description('Windows Router Machine Name') +param virtualMachineName string + @description('VM size') param virtualMachineSize string = 'Standard_B2s' -@description('Windows Router Machine Name') -param virtualMachineName string +@description('Windows Server version. All options are Server Core, small disk, Generation 2 images.') +@allowed(['2019', '2022', '2025']) +param osVersion string = '2025' @description('Select Disk Type: Premium SSD (Premium_LRS), Standard SSD (StandardSSD_LRS), Standard HDD (Standard_LRS)') @allowed([ @@ -12,10 +16,6 @@ param virtualMachineName string ]) param osDiskType string = 'Standard_LRS' -@description('Windows Server version. All options are Server Core, small disk, Generation 2 images.') -@allowed(['2019', '2022', '2025']) -param osVersion string = '2025' - @description('Admin username') param adminUsername string @@ -23,20 +23,25 @@ param adminUsername string @secure() param adminPassword string -@description('Existing Virtual Network Name') -param existingVirtualNetworkName string +@description('How the router is attached to the network. ExistingSubnet: join a virtual network and subnet that already exist. NewSubnet: join an existing virtual network and create a new subnet in it. NewVnet: create a new virtual network and subnet.') +@allowed([ + 'ExistingSubnet' + 'NewSubnet' + 'NewVnet' +]) +param networkMode string = 'ExistingSubnet' -@description('Type Existing Subnet Name') -param existingSubnet string +@description('Virtual network name. Must already exist for ExistingSubnet and NewSubnet; created for NewVnet.') +param virtualNetworkName string -@description('Script that will be executed') -param scriptUri string = uri(deployment().properties.templateLink.uri, '../../scripts/windows/winrouter.ps1') +@description('Address space for the virtual network. Only used when networkMode is NewVnet.') +param virtualNetworkAddressPrefix string = '10.100.0.0/16' -@description('Command to run the script') -param scriptCmd string = 'powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1' +@description('Subnet name. Must already exist for ExistingSubnet; created for NewSubnet and NewVnet.') +param subnetName string -@description('Azure region for all resources.') -param location string = resourceGroup().location +@description('CIDR for the subnet to create, can be as small as /29. Only used when networkMode is NewSubnet or NewVnet.') +param subnetAddressPrefix string = '10.100.0.0/24' @description('Deploy Public IP Address') param deployPublicIpAddress bool = true @@ -44,12 +49,47 @@ param deployPublicIpAddress bool = true @description('Source address prefix allowed to reach the VM on TCP 3389, for example 203.0.113.4/32. Standard SKU public IPs deny inbound traffic by default, so leave this empty only if you do not need RDP from the internet. Use Internet to allow any source (not recommended).') param allowRdpFromAddressPrefix string = '' +@description('Script that will be executed. Defaults to the script alongside this template when deployed from a URL, and to the master branch on GitHub when deployed from a local file or a template spec.') +param scriptUri string = uri(deployment().properties.?templateLink.?uri ?? 'https://raw.githubusercontent.com/dmauser/AzureVM-Router/master/infra/arm/windows-router.json', '../../scripts/windows/winrouter.ps1') + +@description('Command to run the script') +param scriptCmd string = 'powershell.exe -ExecutionPolicy Unrestricted -File winrouter.ps1' + +@description('Azure region for all resources.') +param location string = resourceGroup().location + var extensionName = 'CustomScript' var nicName = '${virtualMachineName}-NIC' var nsgName = '${virtualMachineName}-NSG' var publicIPAddressName = '${virtualMachineName}-PublicIP' -var subnetResourceId = resourceId('Microsoft.Network/virtualNetworks/subnets', existingVirtualNetworkName, existingSubnet) -var deployNetworkSecurityGroup = !empty(allowRdpFromAddressPrefix) + +var createVirtualNetwork = networkMode == 'NewVnet' +var createSubnet = networkMode != 'ExistingSubnet' + +// When the template creates the subnet it owns the subnet NSG. When joining a +// subnet that already exists, the NSG goes on the NIC instead so that any NSG +// already associated with that subnet is left untouched. +var attachNsgToSubnet = createSubnet +var attachNsgToNic = !createSubnet && !empty(allowRdpFromAddressPrefix) +var deployNetworkSecurityGroup = attachNsgToSubnet || attachNsgToNic + +var subnetResourceId = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName) + +var rdpSecurityRules = empty(allowRdpFromAddressPrefix) ? [] : [ + { + name: 'Allow-RDP-Inbound' + properties: { + priority: 200 + protocol: 'Tcp' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: allowRdpFromAddressPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '3389' + } + } +] var osVersionDefinitions = { '2019': { @@ -76,20 +116,7 @@ resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-0 name: nsgName location: location properties: { - securityRules: [ - { - name: 'Allow-RDP-Inbound' - properties: { - priority: 200 - protocol: 'Tcp' - access: 'Allow' - direction: 'Inbound' - sourceAddressPrefix: allowRdpFromAddressPrefix - sourcePortRange: '*' - destinationAddressPrefix: '*' - destinationPortRange: '3389' - } - } + securityRules: concat(rdpSecurityRules, [ { name: 'Allow-Traffic-RFC-1918' properties: { @@ -107,8 +134,86 @@ resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-0 destinationPortRange: '*' } } + ]) + } +} + +resource newVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = if (createVirtualNetwork) { + name: virtualNetworkName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + virtualNetworkAddressPrefix + ] + } + subnets: [ + { + name: subnetName + properties: { + addressPrefix: subnetAddressPrefix + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } + } + ] + } +} + +resource targetVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: virtualNetworkName +} + +resource addedSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = if (networkMode == 'NewSubnet') { + parent: targetVirtualNetwork + name: subnetName + properties: { + addressPrefix: subnetAddressPrefix + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } +} + +resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployPublicIpAddress) { + name: publicIPAddressName + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { + name: nicName + location: location + properties: { + enableIPForwarding: true + networkSecurityGroup: attachNsgToNic ? { + id: networkSecurityGroup.id + } : null + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: subnetResourceId + } + privateIPAllocationMethod: 'Dynamic' + publicIPAddress: deployPublicIpAddress ? { + id: publicIpAddress.id + } : null + } + } ] } + dependsOn: [ + newVirtualNetwork + addedSubnet + ] } resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { @@ -154,42 +259,6 @@ resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = { } } -resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { - name: nicName - location: location - properties: { - enableIPForwarding: true - networkSecurityGroup: deployNetworkSecurityGroup ? { - id: networkSecurityGroup.id - } : null - ipConfigurations: [ - { - name: 'ipconfig1' - properties: { - subnet: { - id: subnetResourceId - } - privateIPAllocationMethod: 'Dynamic' - publicIPAddress: deployPublicIpAddress ? { - id: publicIpAddress.id - } : null - } - } - ] - } -} - -resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployPublicIpAddress) { - name: publicIPAddressName - location: location - sku: { - name: 'Standard' - } - properties: { - publicIPAllocationMethod: 'Static' - } -} - resource virtualMachineExtension 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = { parent: virtualMachine name: extensionName @@ -207,3 +276,12 @@ resource virtualMachineExtension 'Microsoft.Compute/virtualMachines/extensions@2 } } } + +@description('Private IP address of the router, use this as the next hop in a route table.') +output privateIpAddress string = nic.properties.ipConfigurations[0].properties.privateIPAddress + +@description('Public IP address of the router, empty when deployPublicIpAddress is false.') +output publicIpAddress string = publicIpAddress.?properties.ipAddress ?? '' + +@description('Resource ID of the subnet the router is attached to.') +output subnetId string = subnetResourceId diff --git a/tools/publish-templatespecs.sh b/tools/publish-templatespecs.sh new file mode 100644 index 0000000..913bab6 --- /dev/null +++ b/tools/publish-templatespecs.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Publish the router templates as Azure Template Specs. +# +# Template specs give you versioned, RBAC-controlled copies of the templates +# inside your own subscription, so you can deploy them without depending on +# raw.githubusercontent.com being reachable. +# +# Note on scriptUri: a template spec deployment does not expose a template URL, +# so the templates fall back to fetching their setup script from the master +# branch on GitHub. If your VMs cannot reach GitHub, pass scriptUri explicitly +# and point it at a location you control, for example blob storage. +# +# Usage: +# ./tools/publish-templatespecs.sh [location] +# +# Example: +# ./tools/publish-templatespecs.sh rg-templatespecs 1.0.0 eastus + +set -euo pipefail + +if [ "$#" -lt 2 ]; then + echo "Usage: $0 [location]" >&2 + exit 1 +fi + +resourceGroup="$1" +version="$2" +location="${3:-eastus}" + +repoRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +echo "Ensuring resource group $resourceGroup exists in $location" +az group create --name "$resourceGroup" --location "$location" --output none + +publish() { + local specName="$1" + local templateFile="$2" + local description="$3" + + echo "Publishing $specName version $version" + az ts create \ + --resource-group "$resourceGroup" \ + --name "$specName" \ + --version "$version" \ + --location "$location" \ + --template-file "$templateFile" \ + --description "$description" \ + --yes \ + --output none + + az ts show \ + --resource-group "$resourceGroup" \ + --name "$specName" \ + --version "$version" \ + --query id \ + --output tsv +} + +publish "linux-router" \ + "$repoRoot/infra/arm/linux-router.json" \ + "Ubuntu VM configured as a router / NVA." + +publish "windows-router" \ + "$repoRoot/infra/arm/windows-router.json" \ + "Windows Server VM configured as a router / NVA." + +echo +echo "Done. Deploy one with:" +echo " az deployment group create \\" +echo " --resource-group \\" +echo " --template-spec \\" +echo " --parameters virtualMachineName=nva1 networkMode=NewVnet \\" +echo " virtualNetworkName=vnet1 subnetName=nvasubnet \\" +echo " adminUsername=azureuser"