Skip to content

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):

ResourceName
Virtual Machinevm-eshars-ssrs-nonprod-cus
NICnic-eshars-ssrs-nonprod-cus
NSGnsg-eshars-ssrs-nonprod-cus
OS Diskdisk-eshars-ssrs-nonprod-cus-os
Data Diskdisk-eshars-ssrs-nonprod-cus-data
Resource Grouprg-eshars-compute-nonprod-cus (created by workload-core; holds all tier VMs)

Environments

EnvVMResource GroupStatic IPAccess
nonprodvm-eshars-ssrs-nonprod-cus (Standard_D2s_v5)rg-eshars-compute-nonprod-cus10.22.1.6P2S VPN, or npreports.myeshars.com via App Gateway
prodvm-eshars-ssrs-prod-cus (pending)rg-eshars-compute-prod-cus10.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 # TBD

Deployment

Prerequisites

  1. Resource group must already exist (rg-eshars-compute-nonprod-cus, created by workload-core)
  2. SSRS installer pre-staged in the tier staging storage account (saesharsnonprodcus) → scripts container → SQLServerReportingServices.exe (~137 MB — upload manually, not via Tofu). The scripts_storage_* vars must point here; they no longer default to the legacy saesharstfstate account (that silent coupling was removed in the 2026-06 rebuild).
  3. 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 no TF_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:

Terminal window
just init-rs nonprod
just plan-rs nonprod
just apply-rs nonprod
just destroy-rs nonprod # tears down VM, disks, NIC, NSG — NOT the RG

Equivalent raw OpenTofu commands (run from infrastructure/vms/report-server/):

Terminal window
tofu init -backend-config=environments/nonprod.backend.hcl
tofu plan -var-file=environments/nonprod.tfvars
tofu apply -var-file=environments/nonprod.tfvars

Verify

  1. Connect via P2S VPN
  2. 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
  3. Check install log: C:\Temp\install-ssrs.log
  4. Browse SSRS web portal: http://<private-ip>/Reports
  5. Browse SSRS web service: http://<private-ip>/ReportServer
  6. 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).

StepWhat happens
1. Data diskDetects raw disk at LUN 0, formats GPT/NTFS, assigns F:, creates F:\SQLData and F:\SSRSData
2. SQL Express 2022Downloads via Microsoft bootstrapper → extracts media → silent install with SQLEXPRESS instance, TCP enabled, Windows Auth only, data on F:
3. SSRS 2022Pre-staged SQLServerReportingServices.exe installed silently with /Edition=EXPR (Express — perpetual free license)
4. Configure SSRSVia WMI (MSReportServer_ConfigurationSetting): creates ReportServer database on localhost\SQLEXPRESS, sets virtual directories, reserves http://+:80 for both ReportServer and Reports apps
5. FirewallOpens ports 80 and 443 via New-NetFirewallRule
6. Windows Admin CenterInstalled on port 6516
7. TailscaleInstalled 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-core only when both deploy_app_gateway = true and the report-server remote state exposes a non-null private_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_state reads stored state, not code — see INFRA-0010.)
  • The probe path is /Reports (the Web Portal), not the REST API path. Match codes 200-401 accept the unauthenticated 302/401 challenge a healthy portal returns.
  • A separate legacy backend reports-nonprod-backend still exists on its own listener (nonprodreports.myeshars.com10.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 at npreports (the new one).
  • Prod-tier AG ingress is not deployed yetprod.tfvars has deploy_app_gateway = false and no appgw_backends/ssrs_host_name. uat/training/prod apps reference reports.myeshars.com, which has no listener until the prod gateway lands.

Verify backend health:

Terminal window
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 Healthy

Networking & Access

MethodPath
App Gateway (internet)https://npreports.myeshars.com → AG → 10.22.1.6:80/Reports
P2S VPNConnect 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)
RDPVia P2S VPN — RDP to 10.22.1.6
CD pipelineThe 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\Administrators as sysadmin)
  • RDP: P2S VPN only — no public RDP (Bastion was decommissioned 2026-04-17)
  • Scripts storage: CSE uses a SAS-equivalent protected_settings blob access — key is not logged
  • Disk encryption: platform-managed at rest (default)

Troubleshooting

Custom Script Extension failed

Terminal window
# 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-ssrs

RDP 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

Terminal window
Get-Service SQLServerReportingServices # should be Running
netsh http show urlacl # verify http://+:80/ reservations
Get-NetFirewallRule -DisplayName "SSRS*" # verify firewall rules

Re-running the install script

The CSE can only run once per extension name. To re-run:

Terminal window
tofu taint 'module.report_server.azurerm_virtual_machine_extension.install_ssrs'
just apply-rs nonprod

Or destroy and re-apply for a full clean build:

Terminal window
just destroy-rs nonprod
just apply-rs nonprod