Skip to content

Commit bf6426c

Browse files
authored
Merge pull request #1490 from claestom/add-sql-license-type-policies
Add SQL license type compliance Azure Policy samples
2 parents 0c0ffb8 + 09b4107 commit bf6426c

16 files changed

Lines changed: 2457 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Azure Data Factory SSIS Integration Runtime License Type Configuration with Azure Policy
2+
3+
This solution deploys and remediates a custom Azure Policy that audits the `licenseType` property on Azure Data Factory Managed **SSIS Integration Runtimes** (`Microsoft.DataFactory/factories/integrationRuntimes`) and provides a companion script to bring them to a selected target value.
4+
5+
## What Is In This Folder
6+
7+
- `policy/azurepolicy.json`: Custom policy definition (AuditIfNotExists).
8+
- `scripts/deployment.ps1`: Creates/updates the policy definition and policy assignment.
9+
- `scripts/start-remediation.ps1`: Enumerates non-compliant SSIS Integration Runtimes and updates them via `Set-AzDataFactoryV2IntegrationRuntime`.
10+
11+
## License Type Mapping
12+
13+
| Parameter value | Portal label | API `licenseType` |
14+
|---|---|---|
15+
| `LicenseIncluded` | Pay-as-you-go | `LicenseIncluded` |
16+
| `BasePrice` | Azure Hybrid Benefit | `BasePrice` |
17+
18+
> **Note:** Only **Managed** Integration Runtimes with `ssisProperties` (i.e., SSIS-enabled IRs) are in scope. Self-Hosted IRs and Managed IRs without `ssisProperties` (data movement / managed VNet only) do not consume a SQL license and are ignored by both the policy and the remediation script.
19+
20+
## Why AuditIfNotExists Instead Of DeployIfNotExists
21+
22+
The four sibling packs (`sql-arc`, `sql-mi`, `sql-iaas`, `sql-paas`) use `DeployIfNotExists` with an embedded ARM template that PATCHes a single license property. That works because their resource providers merge nested properties on PUT.
23+
24+
The Azure Data Factory resource provider does **not**: `properties.typeProperties` on `Microsoft.DataFactory/factories/integrationRuntimes` is a discriminated union, and a partial PUT through ARM would null out `computeProperties` (node size, count, VNet, etc.) and break the runtime. The official Microsoft sample [`enable-payg-for-azure-sql.ps1`](https://github.com/microsoft/sql-server-samples/blob/master/samples/manage/enable-payg-for-azure-sql/enable-payg-for-azure-sql.ps1) avoids this by calling `Set-AzDataFactoryV2IntegrationRuntime`, which performs an internal GET → merge → PUT against the data plane.
25+
26+
The `Modify` effect is also not available: the `Managed.typeProperties.ssisProperties.licenseType` alias is **not** marked `Modifiable`.
27+
28+
This pack therefore:
29+
30+
- **Audits** drift via the policy assignment (so SSIS IRs show up in the compliance dashboard alongside the other SQL workloads).
31+
- **Remediates** out-of-band via `scripts/start-remediation.ps1`, which mirrors the Microsoft sample.
32+
33+
## Licensing Conditions
34+
35+
When selecting Azure Hybrid Benefit, ensure you meet the licensing requirements:
36+
37+
- **Azure Hybrid Benefit** (`BasePrice`): *"I confirm that I have a SQL Server License with Software Assurance to apply this Azure Hybrid Benefit for SQL Server."*
38+
39+
The deployment script will prompt for confirmation when targeting `BasePrice`. Use `-SkipLicenseConfirmation` to suppress the prompt in automated pipelines (the operator assumes responsibility for license compliance).
40+
41+
## Prerequisites
42+
43+
- PowerShell with Az modules installed (`Az.Resources` for deployment; `Az.DataFactory` for remediation).
44+
- Logged in to Azure (`Connect-AzAccount`).
45+
- Permissions to create policy definitions/assignments at target scope.
46+
- For remediation: Contributor on each Data Factory whose Integration Runtimes will be updated.
47+
48+
## Deploy Policy
49+
50+
Parameter reference:
51+
52+
| Parameter | Required | Default | Allowed values | Description |
53+
|---|---|---|---|---|
54+
| `ManagementGroupId` | No | Tenant root group | Any valid management group ID | Scope where the policy definition is created. Defaults to the tenant root management group when not specified. |
55+
| `SubscriptionId` | No | Not set | Any valid subscription ID | If provided, policy assignment scope is the subscription. |
56+
| `TargetLicenseType` | Yes | N/A | `LicenseIncluded`, `BasePrice` | Target license type to enforce. |
57+
| `LicenseTypesToOverwrite` | No | All | `LicenseIncluded`, `BasePrice` | Select which current license states are eligible for update. |
58+
| `SkipLicenseConfirmation` | No | `false` | Switch (`present`/`not present`) | Skip the interactive license confirmation prompt (for CI/CD pipelines). |
59+
60+
Definition and assignment creation:
61+
62+
1. Download the required files.
63+
64+
```powershell
65+
# Optional: create and enter a local working directory
66+
mkdir sa-sql-ssis-policy
67+
cd sa-sql-ssis-policy
68+
```
69+
70+
```powershell
71+
$baseUrl = "https://raw.githubusercontent.com/microsoft/sql-server-samples/master/samples/manage/azure-data-factory-ssis/sql-ssis-license-type-compliance"
72+
73+
New-Item -ItemType Directory -Path policy, scripts -Force | Out-Null
74+
75+
curl -sLo policy/azurepolicy.json "$baseUrl/policy/azurepolicy.json"
76+
curl -sLo scripts/deployment.ps1 "$baseUrl/scripts/deployment.ps1"
77+
curl -sLo scripts/start-remediation.ps1 "$baseUrl/scripts/start-remediation.ps1"
78+
```
79+
80+
> **Note:** On Windows PowerShell 5.1, `curl` is an alias for `Invoke-WebRequest`. Use `curl.exe` instead, or run the commands in PowerShell 7+.
81+
82+
2. Login to Azure.
83+
84+
```powershell
85+
Connect-AzAccount
86+
```
87+
88+
3. Set your variables. Only `TargetLicenseType` is required — all others are optional.
89+
90+
```powershell
91+
# ── Required ──
92+
$TargetLicenseType = "LicenseIncluded" # "LicenseIncluded" or "BasePrice"
93+
94+
# ── Optional (uncomment to override defaults) ──
95+
# $ManagementGroupId = "<management-group-id>" # Default: tenant root management group
96+
# $SubscriptionId = "<subscription-id>" # Default: policy assigned at management group scope
97+
# $LicenseTypesToOverwrite = @("LicenseIncluded","BasePrice") # Default: all
98+
```
99+
100+
4. Run the deployment.
101+
102+
```powershell
103+
# Minimal — uses defaults for management group and overwrite targets
104+
.\scripts\deployment.ps1 -TargetLicenseType $TargetLicenseType
105+
106+
# With subscription scope
107+
.\scripts\deployment.ps1 -TargetLicenseType $TargetLicenseType -SubscriptionId $SubscriptionId
108+
109+
# With all options
110+
.\scripts\deployment.ps1 `
111+
-ManagementGroupId $ManagementGroupId `
112+
-SubscriptionId $SubscriptionId `
113+
-TargetLicenseType $TargetLicenseType `
114+
-LicenseTypesToOverwrite $LicenseTypesToOverwrite
115+
```
116+
117+
This will:
118+
* Create/update the policy definition at the management group scope.
119+
* Create/assign the policy (at subscription scope when `-SubscriptionId` is provided, otherwise at management group scope).
120+
* Audit (no system-assigned identity is created — `AuditIfNotExists` does not require one) all Managed SSIS Integration Runtimes whose current `licenseType` is in `LicenseTypesToOverwrite` and does not match `TargetLicenseType`.
121+
122+
**Scenario examples:**
123+
124+
```powershell
125+
# Audit all SSIS IRs against Pay-as-you-go
126+
.\scripts\deployment.ps1 -TargetLicenseType "LicenseIncluded"
127+
128+
# Audit only IRs currently on Pay-as-you-go against Azure Hybrid Benefit
129+
.\scripts\deployment.ps1 -TargetLicenseType "BasePrice" -LicenseTypesToOverwrite @("LicenseIncluded")
130+
```
131+
132+
## Start Remediation
133+
134+
Unlike the sibling packs, remediation does **not** use `Start-AzPolicyRemediation` — see [Why AuditIfNotExists Instead Of DeployIfNotExists](#why-auditifnotexists-instead-of-deployifnotexists). Instead, `scripts/start-remediation.ps1` enumerates SSIS IRs in scope and updates each one via `Set-AzDataFactoryV2IntegrationRuntime`.
135+
136+
Parameter reference:
137+
138+
| Parameter | Required | Default | Allowed values | Description |
139+
|---|---|---|---|---|
140+
| `ManagementGroupId` | No | Tenant root group | Any valid management group ID | Enumerates all subscriptions under this management group when `SubscriptionId` is not specified. |
141+
| `SubscriptionId` | No | Not set | Any valid subscription ID | If provided, remediation is limited to this subscription. |
142+
| `TargetLicenseType` | Yes | N/A | `LicenseIncluded`, `BasePrice` | Must match the assignment target license type. |
143+
| `LicenseTypesToOverwrite` | No | All | `LicenseIncluded`, `BasePrice` | Filter which current license states are eligible for update. Should match the value used at deployment time. |
144+
| `Force` | No | `false` | Switch (`present`/`not present`) | Skip the interactive confirmation that lists candidates before applying updates. |
145+
146+
1. Set your variables. `TargetLicenseType` is required and must match the value used during deployment — all others are optional.
147+
148+
```powershell
149+
# ── Required ──
150+
$TargetLicenseType = "LicenseIncluded" # Must match the deployment target
151+
152+
# ── Optional (uncomment to override defaults) ──
153+
# $ManagementGroupId = "<management-group-id>" # Default: tenant root management group
154+
# $SubscriptionId = "<subscription-id>" # Default: scans all subscriptions under the management group
155+
# $LicenseTypesToOverwrite = @("LicenseIncluded","BasePrice") # Default: all
156+
```
157+
158+
2. Run the remediation.
159+
160+
```powershell
161+
# Minimal — scans all subscriptions under the tenant root management group
162+
.\scripts\start-remediation.ps1 -TargetLicenseType $TargetLicenseType
163+
164+
# With subscription scope
165+
.\scripts\start-remediation.ps1 -TargetLicenseType $TargetLicenseType -SubscriptionId $SubscriptionId
166+
167+
# With all options (non-interactive)
168+
.\scripts\start-remediation.ps1 `
169+
-ManagementGroupId $ManagementGroupId `
170+
-SubscriptionId $SubscriptionId `
171+
-TargetLicenseType $TargetLicenseType `
172+
-LicenseTypesToOverwrite $LicenseTypesToOverwrite `
173+
-Force
174+
```
175+
176+
The script:
177+
178+
1. Enumerates Data Factories and Integration Runtimes in scope.
179+
2. Filters to Managed SSIS IRs whose `LicenseType` does not match `TargetLicenseType` and whose current value is in `LicenseTypesToOverwrite`.
180+
3. Prints the candidate list and prompts for confirmation (skip with `-Force`).
181+
4. Calls `Set-AzDataFactoryV2IntegrationRuntime -LicenseType <target> -Force` against each candidate (the cmdlet performs the required GET → merge → PUT internally).
182+
5. Reports per-IR success or failure.
183+
184+
> **Note:** `Set-AzDataFactoryV2IntegrationRuntime` requires the caller to have at least **Data Factory Contributor** (or Contributor) on each factory. The script runs in the interactive Az context — there is no managed identity involved.
185+
186+
## Scope
187+
188+
The policy targets `Microsoft.DataFactory/factories/integrationRuntimes` resources and is filtered to:
189+
190+
- IRs whose `type` is `Managed`.
191+
- IRs that have `typeProperties.ssisProperties` (i.e., SSIS-enabled). Self-Hosted IRs and Managed IRs without SSIS properties are out of scope.
192+
193+
## Reference
194+
195+
- Microsoft sample script (origin of this pattern): [enable-payg-for-azure-sql.ps1](https://github.com/microsoft/sql-server-samples/blob/master/samples/manage/enable-payg-for-azure-sql/enable-payg-for-azure-sql.ps1)
196+
- Azure Data Factory: [Configure Azure-SSIS Integration Runtime](https://learn.microsoft.com/azure/data-factory/create-azure-ssis-integration-runtime)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
{
2+
"displayName": "Audit Azure Data Factory SSIS Integration Runtime license type",
3+
"policyType": "Custom",
4+
"mode": "All",
5+
"description": "Audits Azure Data Factory Managed SSIS Integration Runtimes whose licenseType does not match the target value (LicenseIncluded = PAYG, BasePrice = Azure Hybrid Benefit). Remediate non-compliant IRs by running start-remediation.ps1 (see metadata.helpLink) during a maintenance window: stop the IR, set licenseType, restart. DeployIfNotExists is not supported because the ARM PUT requires the IR to be in Initial or Stopped state.",
6+
"metadata": {
7+
"category": "Data Factory",
8+
"helpLink": "https://github.com/microsoft/sql-server-samples/blob/master/samples/manage/azure-data-factory-ssis/sql-ssis-license-type-compliance/scripts/start-remediation.ps1",
9+
"source": "https://github.com/microsoft/sql-server-samples/tree/master/samples/manage/azure-data-factory-ssis/sql-ssis-license-type-compliance"
10+
},
11+
"parameters": {
12+
"effect": {
13+
"type": "String",
14+
"metadata": {
15+
"displayName": "Effect",
16+
"description": "Enable or disable the execution of the policy."
17+
},
18+
"allowedValues": [
19+
"AuditIfNotExists",
20+
"Disabled"
21+
],
22+
"defaultValue": "AuditIfNotExists"
23+
},
24+
"targetLicenseType": {
25+
"type": "String",
26+
"metadata": {
27+
"displayName": "Target license type",
28+
"description": "License type to enforce on the SSIS Integration Runtime. LicenseIncluded = Pay-as-you-go, BasePrice = Azure Hybrid Benefit."
29+
},
30+
"allowedValues": [
31+
"LicenseIncluded",
32+
"BasePrice"
33+
],
34+
"defaultValue": "LicenseIncluded"
35+
},
36+
"licenseTypesToOverwrite": {
37+
"type": "Array",
38+
"metadata": {
39+
"displayName": "Current license types to overwrite",
40+
"description": "Select which current license type states are eligible for update."
41+
},
42+
"allowedValues": [
43+
"LicenseIncluded",
44+
"BasePrice"
45+
],
46+
"defaultValue": [
47+
"LicenseIncluded",
48+
"BasePrice"
49+
]
50+
}
51+
},
52+
"policyRule": {
53+
"if": {
54+
"allOf": [
55+
{
56+
"field": "type",
57+
"equals": "Microsoft.DataFactory/factories/integrationRuntimes"
58+
},
59+
{
60+
"field": "Microsoft.DataFactory/factories/integrationruntimes/type",
61+
"equals": "Managed"
62+
},
63+
{
64+
"field": "Microsoft.DataFactory/factories/integrationRuntimes/Managed.typeProperties.ssisProperties.licenseType",
65+
"exists": "true"
66+
}
67+
]
68+
},
69+
"then": {
70+
"effect": "[parameters('effect')]",
71+
"details": {
72+
"type": "Microsoft.DataFactory/factories/integrationRuntimes",
73+
"name": "[field('fullName')]",
74+
"existenceCondition": {
75+
"anyOf": [
76+
{
77+
"field": "Microsoft.DataFactory/factories/integrationRuntimes/Managed.typeProperties.ssisProperties.licenseType",
78+
"equals": "[parameters('targetLicenseType')]"
79+
},
80+
{
81+
"allOf": [
82+
{
83+
"field": "Microsoft.DataFactory/factories/integrationRuntimes/Managed.typeProperties.ssisProperties.licenseType",
84+
"equals": "LicenseIncluded"
85+
},
86+
{
87+
"value": "[contains(parameters('licenseTypesToOverwrite'), 'LicenseIncluded')]",
88+
"equals": false
89+
}
90+
]
91+
},
92+
{
93+
"allOf": [
94+
{
95+
"field": "Microsoft.DataFactory/factories/integrationRuntimes/Managed.typeProperties.ssisProperties.licenseType",
96+
"equals": "BasePrice"
97+
},
98+
{
99+
"value": "[contains(parameters('licenseTypesToOverwrite'), 'BasePrice')]",
100+
"equals": false
101+
}
102+
]
103+
}
104+
]
105+
}
106+
}
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)