Azure Policy の Deny ルールをテストする

azure
Published: 2025-09-25

はじめに

Azure Policy には Deny という仕組みがあります。Deny を利用することで、条件に一致するリソースの作成を禁止できます。

Azure Policy 定義の deny 効果

要件を実現するポリシー定義を Microsoft が提供していない場合には、利用者自身がカスタムなポリシー定義を作る必要があります。ポリシー定義を作ること自体は AI の支援もあってかなり楽になったのですが、作ったポリシー定義が実際にリソースの作成を拒否するかどうかの動作確認が面倒です。一番気軽なテストは Azure ポータルから条件に該当するリソースを作る方法です。実に分かりやすいのですが、次のようなめんどくささがあります。

  • テストの証跡を取るには画面キャプチャしなければならない
  • 拒否されるパターンと通るパターンをテストするためには、ほぼ同じ設定のリソースの作成作業を2回やらないといけない
  • ポリシー定義にミスがあると、動作確認の際にリソースが作られてしまう
    • 経験則上、ポータルでリソースの作成が始まる前に検証が行われるリソースと、リソースの作成が開始した後に検証が行われるリソースが存在する。そのため、テストのためには作成ボタンを押さなければならないケースが存在する。
    • 作成ボタンを押す必要のあるリソースのポリシー定義が間違っていると、実際にリソースが作られてしまいわずかではあるがお金が発生してしまう。さらに、作成されてしまったリソースを削除する手間も発生する

というわけで「Azure Resource Manager の仕組みと既存のテストツールを使って、Deny のテストをいい感じにできないものか?」という閃きに挑戦したのが本エントリーです。

ポータルでリソースを作らずに検証する

リソースを作成する直前の「確認と作成」ボタンを押した後に次のような警告が出る場合があります。

ポータルでの警告画面

この挙動には Deployments - Validate の API が利用されています。「確認と作成」をクリックすると、利用者が入力した値を含む ARM テンプレートの情報が Validate の API に渡されます。そして、そのテンプレートをデプロイできるかの検証が行われます。テンプレートの内容が Azure Policy の Deny ルールに該当する場合には、検証が失敗して400エラーが返ってきます。

この Validate API を利用すれば、ポータルをポチポチしなくても Deny ルールのテストが出来そうです。

独自のコマンドを作る

この API を利用する Azure Powershell のコマンドがあります。それが Test-AzResourceGroupDeployment です。Azure DDoS ネットワーク保護を拒否するポリシーが有効な環境で DDoS ネットワーク保護を作成する Bicep テンプレートを渡すと次のような結果を返してくれます。

// Create the DDoS Protection Plan (Standard)
resource ddosPlan 'Microsoft.Network/ddosProtectionPlans@2021-05-01' = {
  name: 'ddosstandard'
  location: 'japaneast'
  properties: {}
}
> $res = Test-AzResourceGroupDeployment -ResourceGroupName rg-policy-test -TemplateFile .\test\deny_ddos_standard.build.bicep
> $res                                                           

InvalidTemplateDeployment - The template deployment failed because of policy violation. Please see details for more information 
.
RequestDisallowedByPolicy - Resource 'ddosstandard' was disallowed by policy. Policy identifiers: '[{"policyAssignment":{"name" 
:"Deny Expensive Azure Services","id":"/providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorizat 
ion/policyAssignments/6aa70cc071124961820bde03"},"policyDefinition":{"name":"Deny DDoS Standard","id":"/providers/Microsoft.Man 
agement/managementGroups/platform/providers/Microsoft.Authorization/policyDefinitions/deny-ddos-standard","version":"1.0.0"},"p 
olicySetDefinition":{"name":"Deny Expensive Azure Services","id":"/providers/Microsoft.Management/managementGroups/platform/pro 
viders/Microsoft.Authorization/policySetDefinitions/deny-expensive-services","version":"1.0.0"}}]'.

とてもよさげです。ですが、このコマンドは「どのポリシーに該当したかのか」を含むエラーメッセージ全体を返してくるため、どのポリシーで拒否されたかの確認をするにはちょっと辛いです。

$res.Details

RequestDisallowedByPolicy - Resource 'ddosstandard' was disallowed by policy. Policy identifiers: '[{"policyAssignment":{"name" 
:"Deny Expensive Azure Services","id":"/providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorizat 
ion/policyAssignments/6aa70cc071124961820bde03"},"policyDefinition":{"name":"Deny DDoS Standard","id":"/providers/Microsoft.Man 
agement/managementGroups/platform/providers/Microsoft.Authorization/policyDefinitions/deny-ddos-standard","version":"1.0.0"},"p 
olicySetDefinition":{"name":"Deny Expensive Azure Services","id":"/providers/Microsoft.Management/managementGroups/platform/pro 
viders/Microsoft.Authorization/policySetDefinitions/deny-expensive-services","version":"1.0.0"}}]'.

一方で、素の Validate API は、以下の様にどのポリシーに違反したかを返してくれます。

> ($result.Content | Convertfrom-Json).error.details.additionalInfo.info

evaluationDetails              : @{evaluatedExpressions=System.Object[]}
policyDefinitionId             : /providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policyDefinitions/deny-ddos-standard
policySetDefinitionId          : /providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policySetDefinitions/deny-expensive-services
policyDefinitionReferenceId    : 4644666272271999012
policySetDefinitionName        : deny-expensive-services
policySetDefinitionDisplayName : Deny Expensive Azure Services
policySetDefinitionVersion     : 1.0.0
policyDefinitionName           : Deny-DDoS-Standard
policyDefinitionDisplayName    : Deny DDoS Standard
policyDefinitionVersion        : 1.0.0
policyDefinitionEffect         : deny
policyAssignmentId             : /providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policyAssignments/6aa70cc071124961820bde03
policyAssignmentName           : 6aa70cc071124961820bde03
policyAssignmentDisplayName    : Deny Expensive Azure Services
policyAssignmentScope          : /providers/Microsoft.Management/managementGroups/platform
policyAssignmentParameters     : 
policyExemptionIds             : {}
policyEnrollmentIds            : {}

そこで今回は Validate API を直接叩いてテストに必要な情報だけを返してくれるコマンドを自作します。

$ErrorActionPreference = "Stop"

function Test-AzDenyPolicy {
    param (
        [parameter(mandatory=$true)][string]$resourceBicepPath,
        [parameter(mandatory=$true)][string]$subscriptionId,
        [parameter(mandatory=$true)][string]$resourceGroupName
    )

    $cmd = "bicep build $resourceBicepPath --stdout"
    $templateJson = Invoke-Expression $cmd | Out-String
    $deploymentName = (New-Guid).Guid
    $url = "https://management.azure.com/subscriptions/$subscriptionId/resourcegroups/$resourceGroupName/providers/Microsoft.Resources/deployments/$deploymentName/validate?api-version=2025-04-01"

    $payload = @{
        properties = @{
            mode     = "Incremental"
            template = (ConvertFrom-Json $templateJson)
        }
    } | ConvertTo-Json -Depth 10

    $result = Invoke-AzRestMethod -Uri $url -Method Post -Payload $payload

    $policies = new-object System.Collections.Generic.List[string]
    foreach ($policyDefinitionName in ($result.Content | Convertfrom-Json).error.details.additionalInfo.info.policyDefinitionName) {
        $policies.Add($policyDefinitionName)
    }
    
    $value = [PSCustomObject]@{
        StatusCode = $result.StatusCode
        Message = ($result.Content | Convertfrom-Json).error.message
        Policies = $policies
        Details = ($result.Content | Convertfrom-Json).error.details
    }
    
    return $value
}

このコマンドは、検証に成功したか失敗したかのステータスコード、エラーメッセージ、Deny されたポリシー名、Validate API が返した詳細全体を返します。

> $res = Test-AzDenyPolicy -resourceBicepPath .\test\deny_ddos_standard.build.bicep -resourceGroupName "rg-policy-test" -subscriptionId (Get-AzContext).Subscription.Id
> $res | fl *                                                                                                                       

StatusCode : 400
Message    : The template deployment failed because of policy violation. Please see details for more information.
Policies   : {Deny-DDoS-Standard}
Details    : {@{code=RequestDisallowedByPolicy; target=ddosstandard; message=Resource 'ddosstandard' was disallowed by policy. Policy identifiers: '[{"policyAssignment":{"name":"Deny Expensive Azure Services","id":"/providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policyAssignments/6aa70cc071124961820bde03"},"policyDefinition":{"name":"Deny DDoS Standard","id":"/providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policyDefinitions/deny-ddos-standard","version":"1.0.0"},"
policySetDefinition":{"name":"Deny Expensive Azure Services","id":"/providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policySetDefinitions/deny-expensive-services","version":"1.0.0"}}]'.; additionalInfo=System.Object[]}}

このコマンドを使えば、bicep で定義されているリソースを作ろうとしたときにポリシーに拒否されたか、拒否された場合どのポリシーで拒否されたのかが一目瞭然です。Azure ポータルをポチポチすることから解放されますし、証跡も残しやすいです。インプットが手作業ではなく Bicep なので再利用も簡単です。

Pester を利用する

テスト用のコマンドができたので、Pester 経由で使ってみます。次のようなファイルを作ります。テスト結果が400エラーかどうか、ポリシーに Deny された応答か、Deny されたルールの名前が作成したものかをチェックします。

BeforeAll {
    . $PSScriptRoot/Test-AzDenyPolicy.ps1
    $resourceGroupName = "rg-policy-test"
    $subscriptionId = (Get-AzContext).Subscription.Id
}


describe 'Deny DDoS Standard Creation' {
    BeforeAll {
        $resourceBicepPath = "./test/deny_ddos_standard.build.bicep"
        $result = Test-AzDenyPolicy -resourceBicepPath $resourceBicepPath -subscriptionId $subscriptionId -resourceGroupName $resourceGroupName
    }
    It 'return code is 400' {
        $result.StatusCode | Should -Be 400
    }

    It 'error message contains policy violation' {
        $result.Message | Should -Be "The template deployment failed because of policy violation. Please see details for more information."
    }

    It 'policy name is deny-ddos-standard' {
        $result.Policies | Should -Contain "deny-ddos-standard"
    }
    
}

以下の様に分かりやすく結果が表示されるようになりました。これはよさげ。

> invoke-Pester .\ValidateAzDenyPolicy.test.ps1 -Output Detailed
Pester v5.7.1

Starting discovery in 1 files.
Discovery found 3 tests in 479ms.
Running tests.

Running tests from '\ValidateAzDenyPolicy.test.ps1'
Describing Deny DDoS Standard Creation
  [+] return code is 400 302ms (232ms|70ms)
  [+] error message contains policy violation 63ms (61ms|2ms)
  [+] policy name is deny-ddos-standard 59ms (55ms|3ms)
Tests completed in 8.03s
Tests Passed: 3, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

まとめ

Deployments - Validate の API と Pester を利用して、Azure Policy の Deny ルールのテストを改善してみました。思い付きを発端に始めた割にはそれなりに便利なものができたような気がします。

Note

  • 当サイトは個人のブログです。このブログに示されている見解や意見は個人的なものであり、所属組織の見解や意見を表明するものではありません。
  • 公開情報を踏まえて正確な情報を掲載するよう努めますが、その内容の完全性や正確性、有用性、安全性、最新性について一切保証しません。
  • 添付文章やリンク先などを含む本サイトの内容は作成時点でのものであり、予告なく変更される場合があります。