diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md index d495344159..95b8aebf26 100644 --- a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md +++ b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md @@ -2,13 +2,43 @@ This repo deploys and remediates a custom Azure Policy that configures and enforces Arc-enabled SQL Server extension `LicenseType` to a selected target value (for example `Paid` or `PAYG`). +> **Looking for Windows Server instead?** To activate **Windows Server** Azure benefits (Software Assurance / pay-as-you-go) on Arc machines, use the companion policy: **[Activate Azure Benefits for Windows Arc Machines](https://github.com/Azure/Community-Policy/tree/main/policyDefinitions/Compute/activate-azure-benefits-for-windows-arc-machines)**. This sample is for **Arc-enabled SQL Server** only. + ## What Is In This Folder - `policy/azurepolicy.json`: Custom policy definition (DeployIfNotExists). +- `policy/azurepolicy.portal.json`: Portal paste version of the definition (`properties` contents only) for creating the policy by hand in the Azure portal. - `scripts/deployment.ps1`: Creates/updates the policy definition and policy assignment. - `scripts/start-remediation.ps1`: Starts a remediation task for the created assignment. - `docs/screenshots/`: Visual references. +## Deployment paths at a glance + +| Path | Use when | Files | +|---|---|---| +| **Command line / pipeline** | You want scope, assignment, role grant and remediation handled for you. | `policy/azurepolicy.json` + `scripts/deployment.ps1` + `scripts/start-remediation.ps1` | +| **Azure Portal** | You want to create the definition by hand and assign it yourself. | `policy/azurepolicy.portal.json` | + +## Choosing the license type by edition (compliance) + +`Paid` = **"License with Software Assurance"** and is a **customer attestation** of active SA / a SQL Server subscription. Free editions are **not** SA-eligible and must be `LicenseOnly`. See [Manage licensing and billing of SQL Server enabled by Azure Arc](https://learn.microsoft.com/sql/sql-server/azure-arc/manage-license-billing?view=sql-server-ver17#license-sql-server-instances-by-virtual-cores). + +**Rule:** if a host has **any** Standard or Enterprise instance → `Paid`. If a host has **only** free editions → `LicenseOnly`. Use `PAYG` for pay-as-you-go hourly billing of Standard/Enterprise. + +| Edition / combination | License type | +|---|---| +| Standard | `Paid` | +| Enterprise | `Paid` | +| Developer | `LicenseOnly` | +| Express | `LicenseOnly` | +| Evaluation | `LicenseOnly` | +| Enterprise + Express | `Paid` | +| Enterprise + Developer | `Paid` | +| Standard + Express | `Paid` | +| Standard + Developer | `Paid` | + +> This sample applies **one** `targetLicenseType` per assignment and is **not yet edition-aware**. To stay compliant on a mixed estate, **scope each assignment** to hosts of one category (e.g. an assignment over Standard/Enterprise hosts → `Paid`; another over free-only hosts → `LicenseOnly`). The scripted path (`deployment.ps1`) currently accepts only `Paid`/`PAYG`; the portal file additionally supports `LicenseOnly` and defaults to it (the safe, non-attesting default). Automatic edition/combination detection is tracked in [issue #1492](https://github.com/microsoft/sql-server-samples/issues/1492). + ## Prerequisites - PowerShell with Az modules installed (`Az.Resources`). @@ -107,6 +137,15 @@ This will: > **Note:** `deployment.ps1` automatically grants required roles to the policy assignment managed identity at assignment scope, preventing common `PolicyAuthorizationFailed` errors during DeployIfNotExists deployments. +## Deploy via the Azure Portal (copy & paste) + +Prefer the portal? The **Policy definition → Policy rule** box expects the `properties` contents. The repo's `policy/azurepolicy.json` also carries a read-only `policyType` and a top-level `version` that the portal rejects, so `policy/azurepolicy.portal.json` has those removed (nothing else changed, plus `LicenseOnly` added to the allowed license types). + +1. **Policy → Definitions → + Policy definition.** +2. Set **Definition location**, **Name** (e.g. *Configure Arc-enabled SQL Server license type*), and **Category** = `Azure Arc`. +3. Open [`policy/azurepolicy.portal.json`](./policy/azurepolicy.portal.json), copy its entire contents, clear the **Policy rule** box and paste it in. +4. **Save**, then **Assign**. On the *Parameters* tab choose your **Target license type** per the edition table above. Because the effect is `DeployIfNotExists`, the assignment needs a **system-assigned managed identity + location**; the portal grants the required roles. Create a **remediation task** to update existing instances. + ## Start Remediation Parameter reference: diff --git a/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.portal.json b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.portal.json new file mode 100644 index 0000000000..6e49427d9a --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.portal.json @@ -0,0 +1,301 @@ +{ + "displayName": "Configure Arc-enabled SQL Server license type", + "mode": "Indexed", + "description": "This policy configures the license type for Arc-enabled SQL Server extensions to a specified target value.", + "metadata": { + "category": "Azure Arc" + }, + "parameters": { + "effect": { + "type": "String", + "metadata": { + "displayName": "Effect", + "description": "Enable or disable the execution of the policy." + }, + "allowedValues": [ + "DeployIfNotExists", + "Disabled" + ], + "defaultValue": "DeployIfNotExists" + }, + "sqlServerExtensionTypes": { + "type": "Array", + "metadata": { + "displayName": "SQL Server extension types", + "description": "Arc-enabled SQL Server extension names to target." + }, + "allowedValues": [ + "WindowsAgent.SqlServer", + "LinuxAgent.SqlServer" + ], + "defaultValue": [ + "WindowsAgent.SqlServer", + "LinuxAgent.SqlServer" + ] + }, + "targetLicenseType": { + "type": "String", + "metadata": { + "displayName": "Target license type", + "description": "LicenseType value to enforce on the Arc-enabled SQL Server extension settings." + }, + "allowedValues": [ + "Paid", + "PAYG", + "LicenseOnly" + ], + "defaultValue": "LicenseOnly" + }, + "licenseTypesToOverwrite": { + "type": "Array", + "metadata": { + "displayName": "Current license types to overwrite", + "description": "Select which current LicenseType states are eligible for update. Use 'Unspecified' to include resources where LicenseType is missing." + }, + "allowedValues": [ + "Unspecified", + "Paid", + "PAYG", + "LicenseOnly" + ], + "defaultValue": [ + "Unspecified", + "Paid", + "PAYG", + "LicenseOnly" + ] + } + }, + "policyRule": { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.HybridCompute/machines/extensions" + }, + { + "anyOf": [ + { + "field": "name", + "in": "[parameters('sqlServerExtensionTypes')]" + }, + { + "field": "name", + "like": "*Agent.SqlServer" + } + ] + }, + { + "field": "Microsoft.HybridCompute/machines/extensions/type", + "in": "[parameters('sqlServerExtensionTypes')]" + } + ] + }, + "then": { + "effect": "[parameters('effect')]", + "details": { + "type": "Microsoft.HybridCompute/machines/extensions", + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/7392c568-9289-4bde-aaaa-b7131215889d", + "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" + ], + "name": "[field('fullName')]", + "existenceCondition": { + "anyOf": [ + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', parameters('targetLicenseType'))))]", + "equals": 1 + }, + { + "anyOf": [ + { + "value": "[parameters('targetLicenseType')]", + "notEquals": "PAYG" + }, + { + "allOf": [ + { + "value": "[parameters('targetLicenseType')]", + "equals": "PAYG" + }, + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "ConsentToRecurringPAYG" + } + ] + } + ] + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "notContainsKey": "LicenseType" + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'Unspecified')]", + "equals": false + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', 'Paid')))]", + "equals": 1 + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'Paid')]", + "equals": false + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', 'PAYG')))]", + "equals": 1 + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'PAYG')]", + "equals": false + }, + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "ConsentToRecurringPAYG" + } + ] + }, + { + "allOf": [ + { + "field": "Microsoft.HybridCompute/machines/extensions/settings", + "ContainsKey": "LicenseType" + }, + { + "value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', 'LicenseOnly')))]", + "equals": 1 + }, + { + "value": "[contains(parameters('licenseTypesToOverwrite'), 'LicenseOnly')]", + "equals": false + } + ] + } + ] + }, + "evaluationDelay": "AfterProvisioningSuccess", + "deployment": { + "properties": { + "mode": "incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "extensionName": { + "type": "string", + "metadata": { + "description": "The Resource name of the Arc server extension." + } + }, + "vmLocation": { + "type": "string", + "metadata": { + "description": "The location of the Arc server." + } + }, + "agentName": { + "type": "string", + "metadata": { + "description": "Name of the agent, i.e. WindowsAgent.SQLServer." + } + }, + "existingSettings": { + "type": "object", + "metadata": { + "description": "The existing settings on the extension." + } + }, + "targetLicenseType": { + "type": "string", + "metadata": { + "description": "LicenseType value to set on the extension." + } + }, + "consentTimestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyy-MM-ddTHH:mm:ssZ')]", + "metadata": { + "description": "UTC timestamp for recurring PAYG consent. Auto-generated at deployment time." + } + } + }, + "functions": [], + "variables": { + "vmExtensionPublisher": "Microsoft.AzureData", + "baseSettings": { + "LicenseType": "[parameters('targetLicenseType')]" + }, + "paygConsentSettings": { + "LicenseType": "[parameters('targetLicenseType')]", + "ConsentToRecurringPAYG": { + "Consented": true, + "ConsentTimestamp": "[parameters('consentTimestamp')]" + } + }, + "licenseSettings": "[if(equals(parameters('targetLicenseType'), 'PAYG'), variables('paygConsentSettings'), variables('baseSettings'))]" + }, + "resources": [ + { + "name": "[parameters('extensionName')]", + "type": "Microsoft.HybridCompute/machines/extensions", + "location": "[parameters('vmLocation')]", + "apiVersion": "2022-11-10", + "properties": { + "publisher": "[variables('vmExtensionPublisher')]", + "type": "[parameters('agentName')]", + "settings": "[union(parameters('existingSettings'), variables('licenseSettings'))]" + } + } + ], + "outputs": {} + }, + "parameters": { + "extensionName": { + "value": "[field('fullName')]" + }, + "vmLocation": { + "value": "[field('location')]" + }, + "agentName": { + "value": "[field('name')]" + }, + "existingSettings": { + "value": "[field('Microsoft.HybridCompute/machines/extensions/settings')]" + }, + "targetLicenseType": { + "value": "[parameters('targetLicenseType')]" + } + } + } + } + } + } + } +}