Report Server — SSRS 2022 on Windows Server 2022
Overview
Standalone Azure VM running SQL Server Reporting Services (SSRS) 2022 with SQL Server 2022 Express as the local report catalog database. Provisioned via OpenTofu with its own state file, separate from the core eSHARS infrastructure. Replaces the legacy vm-sql16-np (SQL Server 2016, Windows Server 2012 R2).
Architecture
rg-eshars-compute-nonprod-cus (created by workload-core, holds tier VMs)│├── vm-eshars-ssrs-nonprod-cus (Windows Server 2022 Datacenter, Standard_D2s_v5)│ ├── SQL Server 2022 Express (SQLEXPRESS instance, port 1433 local only)│ ├── SSRS 2022 Express (:80/Reports, :80/ReportServer)│ └── disk-eshars-ssrs-nonprod-cus-data (F: — F:\SQLData, F:\SSRSData)│├── nic-eshars-ssrs-nonprod-cus (static private IP 10.22.1.6 in the spoke vms subnet)└── nsg-eshars-ssrs-nonprod-cus ├── AllowVpnRDP 3389 from 172.16.1.0/24 (P2S VPN clients) ├── AllowVpnSSRS 80 from 172.16.1.0/24 (P2S VPN clients) ├── AllowAppGwSSRS 80 from the App Gateway subnet └── (deny-all default)
Spoke VNet: vnet-eshars-nonprod-cus / vms subnet (10.22.1.0/24)No public IP — internet ingress is via the App Gateway (npreports.myeshars.com)Resource Naming
Follows {type}-eshars-{component}-{tier}-{region} (full tier token):
| Resource | Name |
|---|---|
| Virtual Machine | vm-eshars-ssrs-nonprod-cus |
| NIC | nic-eshars-ssrs-nonprod-cus |
| NSG | nsg-eshars-ssrs-nonprod-cus |
| OS Disk | disk-eshars-ssrs-nonprod-cus-os |
| Data Disk | disk-eshars-ssrs-nonprod-cus-data |
| Resource Group | rg-eshars-compute-nonprod-cus (created by workload-core; holds all tier VMs) |
Environments
| Env | VM | Resource Group | Static IP | Access |
|---|---|---|---|---|
| nonprod | vm-eshars-ssrs-nonprod-cus (Standard_D2s_v5) | rg-eshars-compute-nonprod-cus | 10.22.1.6 | P2S VPN, or npreports.myeshars.com via App Gateway |
| prod | vm-eshars-ssrs-prod-cus (pending) | rg-eshars-compute-prod-cus | 10.11.1.6 (planned) | P2S VPN; prod App Gateway ingress not yet deployed |
IaC File Structure
infrastructure/├── modules/report-server/ # Reusable module│ ├── main.tf # VM, NIC, NSG, disks, CSE│ ├── variables.tf # Module inputs│ ├── outputs.tf # VM ID, private IP, SSRS URLs│ └── scripts/│ └── install-ssrs.ps1 # Custom Script Extension payload└── vms/report-server/ # Standalone root config (per-tier state) ├── main.tf # Provider, backend, data sources, module call ├── variables.tf # Root variables ├── locals.tf # Tags ├── outputs.tf # Pass-through outputs └── environments/ ├── nonprod.tfvars ├── nonprod.backend.hcl ├── prod.tfvars # TBD └── prod.backend.hcl # TBDDeployment
Prerequisites
- Resource group must already exist (
rg-eshars-compute-nonprod-cus, created byworkload-core) - SSRS installer pre-staged in the tier staging storage account (
saesharsnonprodcus) →scriptscontainer →SQLServerReportingServices.exe(~137 MB — upload manually, not via Tofu). Thescripts_storage_*vars must point here; they no longer default to the legacysaesharstfstateaccount (that silent coupling was removed in the 2026-06 rebuild). - No credential inputs. VM admin credentials are not operator inputs
anymore (changed in eshars-ops #81): the username is a defaulted variable
(
report_server_admin_username, not a secret) and the password is generated at apply time (random_password.admin) and written to the tier ReportViewer Key Vault (ReportServerUsername/ReportServerPassword). There are noTF_VAR_vm_admin_*exports — retrieve the password from the RV vault when you need to RDP.
Deploy / Destroy
These commands are defined in the eshars-ops repo justfile:
just init-rs nonprodjust plan-rs nonprodjust apply-rs nonprod
just destroy-rs nonprod # tears down VM, disks, NIC, NSG — NOT the RGEquivalent raw OpenTofu commands (run from infrastructure/vms/report-server/):
tofu init -backend-config=environments/nonprod.backend.hcltofu plan -var-file=environments/nonprod.tfvarstofu apply -var-file=environments/nonprod.tfvarsVerify
- Connect via P2S VPN
- Find the VM’s current private IP:
Terminal window az vm list-ip-addresses -g rg-eshars-compute-nonprod-cus -n vm-eshars-ssrs-nonprod-cus \--query "[].virtualMachine.network.privateIpAddresses[0]" -o tsv - Check install log:
C:\Temp\install-ssrs.log - Browse SSRS web portal:
http://<private-ip>/Reports - Browse SSRS web service:
http://<private-ip>/ReportServer - Check edition in SSRS Configuration Manager — should show Express (not Evaluation)
Installation Process (Custom Script Extension)
install-ssrs.ps1 runs automatically on first boot via Azure VM Custom Script Extension. The SSRS installer binary is downloaded from blob storage alongside the script (CSE places all fileUris in the same working directory before execution).
| Step | What happens |
|---|---|
| 1. Data disk | Detects raw disk at LUN 0, formats GPT/NTFS, assigns F:, creates F:\SQLData and F:\SSRSData |
| 2. SQL Express 2022 | Downloads via Microsoft bootstrapper → extracts media → silent install with SQLEXPRESS instance, TCP enabled, Windows Auth only, data on F: |
| 3. SSRS 2022 | Pre-staged SQLServerReportingServices.exe installed silently with /Edition=EXPR (Express — perpetual free license) |
| 4. Configure SSRS | Via WMI (MSReportServer_ConfigurationSetting): creates ReportServer database on localhost\SQLEXPRESS, sets virtual directories, reserves http://+:80 for both ReportServer and Reports apps |
| 5. Firewall | Opens ports 80 and 443 via New-NetFirewallRule |
| 6. Windows Admin Center | Installed on port 6516 |
| 7. Tailscale | Installed if TF_VAR_tailscale_auth_key is set |
All output logged to C:\Temp\install-ssrs.log.
App Gateway Ingress
The report server has no public IP. Internet ingress is fronted by the shared tier App
Gateway (appgw-eshars-nonprod-cus, owned by workload-core). The SSRS backend is wired
dynamically from the report-server root’s remote state — it is not a hardcoded IP.
npreports.myeshars.com → AG listener reports-nonprod-new-https-listener → routing rule reports-nonprod-new-https-rule → backend pool reports-nonprod-new-backend → 10.22.1.6 (from report-server tfstate) → http settings + probe /Reports (match 200-401)Key facts:
- The backend pool is built in
workload-coreonly when bothdeploy_app_gateway = trueand the report-server remote state exposes a non-nullprivate_ip_address. If report-server is applied after workload-core, re-apply workload-core so it re-reads the now-populated state and registers the backend. (terraform_remote_statereads stored state, not code — see INFRA-0010.) - The probe path is
/Reports(the Web Portal), not the REST API path. Match codes200-401accept the unauthenticated 302/401 challenge a healthy portal returns. - A separate legacy backend
reports-nonprod-backendstill exists on its own listener (nonprodreports.myeshars.com→10.1.0.4, the old SSRS on the legacy 10.1.x VNet). It does not collide with the new one (different hostname) and is a cleanup candidate once the legacy estate is retired. The app config points atnpreports(the new one). - Prod-tier AG ingress is not deployed yet —
prod.tfvarshasdeploy_app_gateway = falseand noappgw_backends/ssrs_host_name. uat/training/prod apps referencereports.myeshars.com, which has no listener until the prod gateway lands.
Verify backend health:
az network application-gateway show-backend-health \ -g rg-eshars-web-nonprod-cus -n appgw-eshars-nonprod-cus \ --query "backendAddressPools[?contains(backendAddressPool.id,'reports-nonprod-new')]"# expect: 10.22.1.6 HealthyNetworking & Access
| Method | Path |
|---|---|
| App Gateway (internet) | https://npreports.myeshars.com → AG → 10.22.1.6:80/Reports |
| P2S VPN | Connect to the nonprod VPN, then browse http://10.22.1.6/Reports (requires DNS-mode enabled / a route to 10.22.0.0/16 — see INFRA-0009) |
| RDP | Via P2S VPN — RDP to 10.22.1.6 |
| CD pipeline | The ops VM MI reaches port 80 directly within the spoke for report-deploy steps |
Security
- VM credentials: Terraform-generated at apply time and stored in the tier ReportViewer Key Vault — never a human/CI input, never in tfvars or env vars
- SQL Express: Windows Authentication only (
BUILTIN\Administratorsas sysadmin) - RDP: P2S VPN only — no public RDP (Bastion was decommissioned 2026-04-17)
- Scripts storage: CSE uses a SAS-equivalent
protected_settingsblob access — key is not logged - Disk encryption: platform-managed at rest (default)
Troubleshooting
Custom Script Extension failed
# Azure portal: VM → Extensions + Applications → install-ssrs → View detailed status# Or via CLI:az vm extension show \ --resource-group rg-eshars-compute-nonprod-cus \ --vm-name vm-eshars-ssrs-nonprod-cus \ --name install-ssrsRDP in and read C:\Temp\install-ssrs.log. SQL Express install logs are at:
C:\Program Files\Microsoft SQL Server\*\Setup Bootstrap\Log\Summary.txt
SSRS not accessible
Get-Service SQLServerReportingServices # should be Runningnetsh http show urlacl # verify http://+:80/ reservationsGet-NetFirewallRule -DisplayName "SSRS*" # verify firewall rulesRe-running the install script
The CSE can only run once per extension name. To re-run:
tofu taint 'module.report_server.azurerm_virtual_machine_extension.install_ssrs'just apply-rs nonprodOr destroy and re-apply for a full clean build:
just destroy-rs nonprodjust apply-rs nonprodRelated
- vm-ssrs16-prod.md — legacy production SSRS VM (pending replacement)
- Change: Nonprod SSRS VM Provision
- Decision: VNet Integration for App Service SSRS