Skip to content

Commit bc4800d

Browse files
committed
Add PSRule graph validation article
1 parent 21e7626 commit bc4800d

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
---
2+
title: "Validate Azure Resource Relationships with PSRule and PowerShell Graphs"
3+
description: "Learn how to combine PSRule for Azure with PSQuickGraph to validate relationships that span multiple Bicep resources, such as VNet integration and private endpoint connectivity."
4+
author: eosfor
5+
authors:
6+
- eosfor
7+
date: 2026-07-24T00:00:00+00:00
8+
categories:
9+
- DevOps
10+
tags:
11+
- psrule
12+
- azure
13+
- bicep
14+
- graph
15+
- infrastructure-as-code
16+
---
17+
18+
PSRule for Azure makes it straightforward to validate the properties of individual
19+
resources before deployment. But some architecture requirements describe a
20+
relationship, not a property: every Function App must connect to the expected virtual
21+
network, or every application must have exactly one private endpoint.
22+
23+
This article shows how to collect those relationships in a PowerShell graph while
24+
PSRule processes a Bicep deployment, then validate the completed graph at the end of
25+
the pipeline.
26+
27+
## Why per-resource rules are not enough
28+
29+
Consider an architecture with a Function App, a virtual network, an integration
30+
subnet, and a private endpoint subnet. We can easily write rules that check:
31+
32+
- whether the Function App has public access disabled;
33+
- whether the virtual network uses the correct address space;
34+
- whether the expected subnets exist.
35+
36+
Those checks still do not prove that the Function App is connected to the correct
37+
subnet or that its private endpoint belongs to the expected virtual network. The
38+
required information is spread across several expanded ARM resources.
39+
40+
A graph is a natural representation of this problem:
41+
42+
- resources and subnets become vertices;
43+
- references between them become edges;
44+
- an architecture requirement becomes a path or edge-count assertion.
45+
46+
## Prepare PSRule and the graph module
47+
48+
The example uses
49+
[PSRule.Rules.Azure](https://github.com/Azure/PSRule.Rules.Azure) to expand and
50+
analyze Bicep and
51+
[PSQuickGraph](https://github.com/eosfor/PSGraph) to build the dependency graph.
52+
53+
```powershell
54+
Install-Module -Name PSRule.Rules.Azure -Scope CurrentUser
55+
Install-Module -Name PSQuickGraph -Scope CurrentUser
56+
57+
Import-Module PSQuickGraph
58+
```
59+
60+
In `ps-rule.yaml`, enable Bicep expansion and include the convention that will collect
61+
relationships:
62+
63+
```yaml
64+
include:
65+
module:
66+
- PSRule.Rules.Azure
67+
- PSQuickGraph
68+
69+
convention:
70+
include:
71+
- FullConnectivityTest
72+
73+
configuration:
74+
AZURE_BICEP_FILE_EXPANSION: true
75+
```
76+
77+
## Collect relationships with a convention
78+
79+
A PSRule
80+
[convention](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Conventions/)
81+
can run custom PowerShell at different stages of the pipeline. Its `Process` block
82+
runs once for each input object, while its `End` block runs after all objects have
83+
been processed.
84+
85+
That lifecycle gives us a convenient two-phase approach:
86+
87+
1. Add every relevant resource and relationship to a graph.
88+
2. Validate the graph only after the complete deployment has been seen.
89+
90+
The following is a simplified version of the convention. The example uses
91+
child-to-parent edges so a valid dependency chain ends at the virtual network.
92+
93+
```powershell
94+
$global:vnetId = $null
95+
$global:webSites = @()
96+
$global:connectionGraph = New-Graph
97+
$global:privateEndpointGraph = New-Graph
98+
99+
Export-PSRuleConvention 'FullConnectivityTest' -Process {
100+
if ($TargetObject.Type -eq 'Microsoft.Network/virtualNetworks') {
101+
$global:vnetId = $TargetObject.Id
102+
103+
foreach ($subnetResource in $TargetObject.Resources) {
104+
Add-Edge `
105+
-From $subnetResource.Id `
106+
-To $TargetObject.Id `
107+
-Graph $global:connectionGraph
108+
109+
Add-Edge `
110+
-From $subnetResource.Id `
111+
-To $TargetObject.Id `
112+
-Graph $global:privateEndpointGraph
113+
}
114+
}
115+
116+
if ($TargetObject.Type -eq 'Microsoft.Web/sites') {
117+
$vnetIntegration = $TargetObject.Resources |
118+
Where-Object Type -eq 'Microsoft.Web/sites/networkConfig'
119+
120+
$global:webSites += $TargetObject.Id
121+
122+
Add-Edge `
123+
-From $TargetObject.Id `
124+
-To $vnetIntegration.Properties.SubnetResourceId `
125+
-Graph $global:connectionGraph
126+
}
127+
128+
if ($TargetObject.Type -eq 'Microsoft.Network/privateEndpoints') {
129+
Add-Edge `
130+
-From $TargetObject.Id `
131+
-To $TargetObject.Properties.Subnet.Id `
132+
-Graph $global:privateEndpointGraph
133+
134+
foreach ($connection in $TargetObject.Properties.PrivateLinkServiceConnections) {
135+
Add-Edge `
136+
-From $connection.Properties.PrivateLinkServiceId `
137+
-To $TargetObject.Id `
138+
-Graph $global:privateEndpointGraph
139+
}
140+
}
141+
} -End {
142+
# The completed graphs are validated here.
143+
}
144+
```
145+
146+
The sample uses global variables because the state must remain available across
147+
PSRule callback invocations. In a larger rule set, wrap this state in a single object
148+
and reset it before each run.
149+
150+
## Validate complete dependency paths
151+
152+
Once PSRule reaches the `End` block, every relevant Bicep resource has been expanded
153+
and processed. We can now ask questions about the deployment as a whole.
154+
155+
For example, the following check confirms that every Function App has a VNet
156+
integration path:
157+
158+
```powershell
159+
foreach ($webApp in $global:webSites) {
160+
$path = Get-GraphPath `
161+
-From $webApp `
162+
-To $global:vnetId `
163+
-Graph $global:connectionGraph
164+
165+
if ($null -eq $path) {
166+
throw "No VNet integration path was found for Function App: $webApp"
167+
}
168+
}
169+
```
170+
171+
We can apply the same idea to private endpoint connectivity:
172+
173+
```powershell
174+
foreach ($webApp in $global:webSites) {
175+
$path = Get-GraphPath `
176+
-From $webApp `
177+
-To $global:vnetId `
178+
-Graph $global:privateEndpointGraph
179+
180+
if ($null -eq $path) {
181+
throw "No private endpoint path was found for Function App: $webApp"
182+
}
183+
}
184+
```
185+
186+
Throwing from the `End` block causes the validation run to fail. This works well in
187+
CI, although it does not produce the same detailed assertion output as a normal
188+
PSRule `Rule` block.
189+
190+
Run the rules against the Bicep entry point with:
191+
192+
```powershell
193+
Invoke-PSRule -Format File -InputPath ./deployments/non-prod/main.bicep
194+
```
195+
196+
## Export the graph for troubleshooting
197+
198+
The same model used for validation can produce a diagram. This is particularly
199+
helpful when a CI check reports a missing path and you need to see which relationship
200+
was absent. PSQuickGraph can export the graph in Graphviz DOT format; the
201+
[Graphviz](https://graphviz.org/download/) `dot` executable can then render it as SVG.
202+
203+
```powershell
204+
Export-Graph `
205+
-Graph $global:privateEndpointGraph `
206+
-Format Graphviz `
207+
-Path ./output/private-endpoints.dot
208+
209+
& dot `
210+
-Tsvg ./output/private-endpoints.dot `
211+
-o ./output/private-endpoints.svg
212+
```
213+
214+
![An Azure Function App connected through a private endpoint and subnet to a virtual network](/images/articles/psrule-azure-resource-relationships.svg)
215+
216+
This separates the solution into three clear steps:
217+
218+
1. PSRule expands Bicep into resource objects.
219+
2. A convention converts cross-resource references into graph edges.
220+
3. Graph queries validate the architecture after all resources are available.
221+
222+
The result complements normal per-resource rules instead of replacing them. Use
223+
regular PSRule assertions for local properties and graph assertions for requirements
224+
that span the deployment.
225+
226+
A complete Bicep project, PSRule configuration, architecture document, and runnable
227+
Codespaces environment are available in the
228+
[psrule-demo repository](https://github.com/eosfor/psrule-demo).
Lines changed: 33 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)