diff --git a/blueprints/full-multi-node-cluster/bicep/main.bicep b/blueprints/full-multi-node-cluster/bicep/main.bicep
index 600cf5d5..25f1494c 100644
--- a/blueprints/full-multi-node-cluster/bicep/main.bicep
+++ b/blueprints/full-multi-node-cluster/bicep/main.bicep
@@ -163,10 +163,8 @@ param shouldCreateAks bool = false
IoT Operations Parameters
*/
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
-// param trustIssuerSettings iotOpsTypes.TrustIssuerConfig = { trustSource: 'SelfSigned' }
-var trustIssuerSettings = { trustSource: 'SelfSigned' }
+@description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
+param trustIssuerSettings types.TrustIssuerConfig = { trustSource: 'SelfSigned' }
@description('Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments)')
param shouldCreateAnonymousBrokerListener bool = false
@@ -177,20 +175,14 @@ param shouldInitAio bool = true
@description('Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster.')
param shouldDeployAio bool = true
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether to deploy DeploymentScripts for Azure IoT Operations.')
-// param shouldDeployAioDeploymentScripts bool = false
-var shouldDeployAioDeploymentScripts = false
+@description('Whether to deploy DeploymentScripts for Azure IoT Operations.')
+param shouldDeployAioDeploymentScripts bool = false
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether or not to enable the Open Telemetry Collector for Azure IoT Operations.')
-// param shouldEnableOtelCollector bool = true
-var shouldEnableOtelCollector = false
+@description('Whether or not to enable the Open Telemetry Collector for Azure IoT Operations.')
+param shouldEnableOtelCollector bool = true
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations.')
-// param shouldEnableOpcUaSimulator bool = true
-var shouldEnableOpcUaSimulator = false
+@description('Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations.')
+param shouldEnableOpcUaSimulator bool = false
/*
Device Configuration Parameters
diff --git a/blueprints/full-single-node-cluster/bicep/README.md b/blueprints/full-single-node-cluster/bicep/README.md
index 9240bf11..e64db843 100644
--- a/blueprints/full-single-node-cluster/bicep/README.md
+++ b/blueprints/full-single-node-cluster/bicep/README.md
@@ -38,9 +38,13 @@ Deploys a complete end-to-end environment for Azure IoT Operations on a single-n
| vpnGatewayAzureAdConfig | Azure AD authentication configuration for VPN Gateway. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no |
| shouldCreateAks | Whether to create an Azure Kubernetes Service cluster. | `bool` | `false` | no |
| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:
az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes |
+| trustIssuerSettings | The trust issuer settings for Customer Managed Azure IoT Operations Settings. | `[_3.TrustIssuerConfig](#user-defined-types)` | {'trustSource': 'SelfSigned'} | no |
| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no |
| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no |
| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no |
+| shouldDeployAioDeploymentScripts | Whether to deploy DeploymentScripts for Azure IoT Operations. | `bool` | `false` | no |
+| shouldEnableOtelCollector | Whether or not to enable the Open Telemetry Collector for Azure IoT Operations. | `bool` | `true` | no |
+| shouldEnableOpcUaSimulator | Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations. | `bool` | `false` | no |
| namespacedDevices | List of namespaced devices to create. | `[_4.NamespacedDevice](#user-defined-types)[]` | [] | no |
| assetEndpointProfiles | List of asset endpoint profiles to create. | `[_4.AssetEndpointProfile](#user-defined-types)[]` | [] | no |
| legacyAssets | List of legacy assets to create. | `[_4.LegacyAsset](#user-defined-types)[]` | [] | no |
diff --git a/blueprints/full-single-node-cluster/bicep/main.bicep b/blueprints/full-single-node-cluster/bicep/main.bicep
index b2326ee6..1aa1a115 100644
--- a/blueprints/full-single-node-cluster/bicep/main.bicep
+++ b/blueprints/full-single-node-cluster/bicep/main.bicep
@@ -7,6 +7,7 @@ import * as assetTypes from '../../../src/100-edge/111-assets/bicep/types.bicep'
import * as messagingTypes from '../../../src/100-edge/130-messaging/bicep/types.bicep'
import * as aiFoundryTypes from '../../../src/000-cloud/085-ai-foundry/bicep/types.bicep'
import * as vpnGatewayTypes from '../../../src/000-cloud/055-vpn-gateway/bicep/types.bicep'
+import * as iotOpsTypes from '../../../src/100-edge/110-iot-ops/bicep/types.bicep'
targetScope = 'subscription'
@@ -153,10 +154,8 @@ param customLocationsOid string
IoT Operations Parameters
*/
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
-// param trustIssuerSettings iotOpsTypes.TrustIssuerConfig = { trustSource: 'SelfSigned' }
-var trustIssuerSettings = { trustSource: 'SelfSigned' }
+@description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
+param trustIssuerSettings iotOpsTypes.TrustIssuerConfig = { trustSource: 'SelfSigned' }
@description('Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments)')
param shouldCreateAnonymousBrokerListener bool = false
@@ -167,22 +166,16 @@ param shouldInitAio bool = true
@description('Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster.')
param shouldDeployAio bool = true
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether to deploy DeploymentScripts for Azure IoT Operations.')
-// param shouldDeployAioDeploymentScripts bool = false
-var shouldDeployAioDeploymentScripts = false
+@description('Whether to deploy DeploymentScripts for Azure IoT Operations.')
+param shouldDeployAioDeploymentScripts bool = false
// No additional resource group parameters needed
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether or not to enable the Open Telemetry Collector for Azure IoT Operations.')
-// param shouldEnableOtelCollector bool = true
-var shouldEnableOtelCollector = false
+@description('Whether or not to enable the Open Telemetry Collector for Azure IoT Operations.')
+param shouldEnableOtelCollector bool = true
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations.')
-// param shouldEnableOpcUaSimulator bool = true
-var shouldEnableOpcUaSimulator = false
+@description('Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations.')
+param shouldEnableOpcUaSimulator bool = false
/*
Device Configuration Parameters
diff --git a/blueprints/only-edge-iot-ops/bicep/main.bicep b/blueprints/only-edge-iot-ops/bicep/main.bicep
index 8d262504..cc9dc818 100644
--- a/blueprints/only-edge-iot-ops/bicep/main.bicep
+++ b/blueprints/only-edge-iot-ops/bicep/main.bicep
@@ -3,6 +3,7 @@ metadata description = 'Deploys Azure IoT Operations on an existing Arc-enabled
import * as core from './types.core.bicep'
import * as assetTypes from '../../../src/100-edge/111-assets/bicep/types.bicep'
+import * as types from '../../../src/100-edge/110-iot-ops/bicep/types.bicep'
/*
Common Parameters
*/
@@ -24,10 +25,8 @@ param customLocationName string = '${arcConnectedClusterName}-cl'
Trust Configuration Parameters
*/
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
-// param trustIssuerSettings types.TrustIssuerConfig = { trustSource: 'SelfSigned' }
-var trustIssuerSettings = { trustSource: 'SelfSigned' }
+@description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
+param trustIssuerSettings types.TrustIssuerConfig = { trustSource: 'SelfSigned' }
/*
Secret Sync and Key Vault Parameters
@@ -64,10 +63,8 @@ param deployUserTokenSecretName string?
@description('The prefix used with constructing the secret name that will have the deployment script.')
param deploymentScriptsSecretNamePrefix string = '${common.resourcePrefix}-${common.environment}-${common.instance}'
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether to deploy DeploymentScripts for Azure IoT Operations.')
-// param shouldDeployAioDeploymentScripts bool = false
-var shouldDeployAioDeploymentScripts = false
+@description('Whether to deploy DeploymentScripts for Azure IoT Operations.')
+param shouldDeployAioDeploymentScripts bool = false
@description('Whether to assign roles to the deploy identity.')
param shouldAssignDeployIdentityRoles bool = true
@@ -107,15 +104,11 @@ param shouldCreateAnonymousBrokerListener bool = false
@description('Whether to deploy Custom Locations Resource Sync Rules for the Azure IoT Operations resources.')
param shouldDeployResourceSyncRules bool = true
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether or not to enable the Open Telemetry Collector for Azure IoT Operations.')
-// param shouldEnableOtelCollector bool = true
-var shouldEnableOtelCollector = false
+@description('Whether or not to enable the Open Telemetry Collector for Azure IoT Operations.')
+param shouldEnableOtelCollector bool = true
-// Currently disable setting shouldDeployAioDeploymentScripts, remove when DeploymentScripts supports AZ CLI 2.71+ (post May 4)
-// @description('Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations.')
-// param shouldEnableOpcUaSimulator bool = true
-var shouldEnableOpcUaSimulator = false
+@description('Whether or not to enable the OPC UA Simulator and deploy ADR Asset for Azure IoT Operations.')
+param shouldEnableOpcUaSimulator bool = false
/*
Device Configuration Parameters
diff --git a/docs/getting-started/general-user.md b/docs/getting-started/general-user.md
index 3967dfed..bdba647a 100644
--- a/docs/getting-started/general-user.md
+++ b/docs/getting-started/general-user.md
@@ -544,12 +544,14 @@ After successful deployment:
1. **Explore the solution**: Familiarize yourself with deployed components
2. **Configure monitoring**: Set up alerts and dashboards in Grafana
3. **Customize settings**: Modify configuration for your specific needs
-4. **Learn more**: Review [Azure IoT Operations documentation][iot-ops-docs] for advanced features
+4. **Upgrade Azure IoT Operations**: When new AIO releases ship, follow the [Upgrade Azure IoT Operations](upgrade-aio.md) guide to upgrade and reconcile with Terraform or Bicep
+5. **Learn more**: Review [Azure IoT Operations documentation][iot-ops-docs] for advanced features
## Additional Resources
- **[Blueprint Developer Guide](blueprint-developer.md)** - Create custom blueprints
- **[Feature Developer Guide](feature-developer.md)** - Contribute to the platform
+- **[Upgrade Azure IoT Operations](upgrade-aio.md)** - Upgrade AIO and reconcile state for Terraform / Bicep
- **[Azure IoT Operations Getting Started][iot-ops-quickstart]** - Official Microsoft guide
- **[Troubleshooting Documentation](../observability/)** - Detailed troubleshooting guides
diff --git a/docs/getting-started/upgrade-aio.md b/docs/getting-started/upgrade-aio.md
new file mode 100644
index 00000000..57b19788
--- /dev/null
+++ b/docs/getting-started/upgrade-aio.md
@@ -0,0 +1,125 @@
+---
+title: Upgrade Azure IoT Operations
+description: How to upgrade Azure IoT Operations (AIO) and reconcile the upgrade with the edge-ai Terraform or Bicep deployments
+author: Edge AI Team
+ms.date: 2026-05-05
+ms.topic: how-to
+estimated_reading_time: 5
+keywords:
+ - azure iot operations
+ - aio upgrade
+ - terraform
+ - bicep
+ - cert-manager
+ - secret store
+---
+
+## Upgrade Azure IoT Operations
+
+This guide describes how to upgrade an Azure IoT Operations (AIO) instance deployed by edge-ai and how to reconcile the upgrade with your Infrastructure as Code (IaC) of choice.
+
+The `az iot ops upgrade` command updates three cluster components when newer stable versions are available:
+
+- `certManager`
+- `secretStore`
+- `iotOperations`
+
+The reconciliation steps differ between Terraform (stateful) and Bicep (stateless).
+
+## Version matrix
+
+This repository currently targets the **AIO 2604** release. The table below maps `az iot ops` CLI versions to the component versions pinned in edge-ai:
+
+| CLI extension (`azure-iot-ops`) | AIO release | cert-manager | secret-sync-controller | iotOperations |
+|---------------------------------|-------------|--------------|------------------------|---------------|
+| 2.5.0 | 2604 | 0.11.0 | 1.4.0 | 1.3.70 |
+
+For the full upstream compatibility matrix, see [Supported versions — Azure IoT Operations](https://learn.microsoft.com/azure/iot-operations/deploy-iot-ops/howto-upgrade?tabs=portal#supported-versions).
+
+## Prerequisites
+
+- Azure CLI logged in to the target subscription.
+- `azure-iot-ops` CLI extension installed (this repo expects version `2.5.0`).
+- `` — the resource group containing the AIO instance.
+- `` — the AIO instance name (for edge-ai blueprints this is typically `iotops-arck---`).
+
+## Run the AIO upgrade
+
+Run the upgrade against your existing AIO instance. Review the proposed changes and confirm:
+
+```bash
+az iot ops upgrade \
+ --resource-group \
+ --name
+```
+
+The CLI prints a table comparing current and desired versions for `certManager`, `secretStore`, and `iotOperations`, then performs the update.
+
+After this point, Azure has been mutated — the steps below bring your IaC back in sync.
+
+## Reconcile with Terraform
+
+Terraform is stateful. The state file holds the previous extension versions; after `az iot ops upgrade` it no longer matches Azure. Refresh state, verify, then re-apply your blueprint at the latest edge-ai versions.
+
+1. Refresh state from Azure to absorb the new versions:
+
+ ```bash
+ cd blueprints//terraform
+ source ../../../scripts/az-sub-init.sh
+ terraform apply -refresh-only -var-file=terraform.tfvars
+ ```
+
+ Confirm when prompted. Terraform updates the in-state `version` attribute for the three extensions to match what Azure now reports.
+
+2. Verify there is no drift on the upgraded components:
+
+ ```bash
+ terraform plan -var-file=terraform.tfvars | grep -E "cert_manager|secret_store|iot_operations"
+ ```
+
+ No output (or `No changes`) means the IaC and Azure agree on the new versions.
+
+3. Re-run edge-ai with the latest AIO version pins and any updated components.
+
+ - Pull the latest edge-ai changes (which may bump the version defaults in [variables.init.tf](../../src/100-edge/110-iot-ops/terraform/variables.init.tf), [variables.instance.tf](../../src/100-edge/110-iot-ops/terraform/variables.instance.tf), and [variables.tf](../../src/100-edge/109-arc-extensions/terraform/variables.tf)).
+ - Apply the blueprint:
+
+ ```bash
+ terraform apply -var-file=terraform.tfvars
+ ```
+
+ If the upstream pins are now newer than what `az iot ops upgrade` installed, Terraform will move the cluster to the newer pins. If they match, the apply is a no-op for the AIO extensions.
+
+> **Pinned releases**: If your team pins to a specific edge-ai release tag rather than `main`, the version defaults in that release may be older than what `az iot ops upgrade` installed.
+> In that case, after `-refresh-only`, `terraform plan` will show no diff for the AIO extensions (state matches Azure). However, if you later move to a newer edge-ai release with higher version pins,
+> the next `apply` will attempt to upgrade again. To stay aligned, either upgrade edge-ai to the release that matches the AIO versions you upgraded to, or override the version variables in your `terraform.tfvars`.
+>
+> If you skip step 1, the next `terraform apply` will detect "drift" on the three extensions and roll Azure back to the versions pinned in code. Always run `-refresh-only` first when you upgraded out-of-band.
+
+## Reconcile with Bicep
+
+Bicep is stateless — there is no per-deployment record of previously applied versions to reconcile. After `az iot ops upgrade` no further ARM operations are required to "import" the new state. Subsequent Bicep deployments are idempotent against whatever exists in Azure.
+
+1. Run the AIO upgrade as shown above.
+
+2. Re-run edge-ai with the latest AIO version pins and any updated components.
+
+ - Pull the latest edge-ai changes (which may bump the defaults in [109-arc-extensions/bicep/types.bicep](../../src/100-edge/109-arc-extensions/bicep/types.bicep) and [110-iot-ops/bicep/types.bicep](../../src/100-edge/110-iot-ops/bicep/types.bicep)).
+ - Re-deploy your blueprint with `az deployment group create` (or your existing pipeline) using the same parameters.
+
+ If the parameter values are equal to or newer than what `az iot ops upgrade` installed, ARM applies the new versions. Otherwise the deployment is a no-op for the AIO extensions.
+
+> **Pinned releases**: If your team pins to a specific edge-ai release tag, ensure the version parameters passed to the blueprint match or exceed what `az iot ops upgrade` installed. If they are lower, the next deployment will attempt to downgrade the extensions. Override the version parameters explicitly or upgrade to an edge-ai release that includes the newer defaults.
+>
+> Because Bicep / ARM compares declared properties to live resource state, you do not need a refresh, import, or state-edit step. The next deployment is the reconciliation.
+
+## Troubleshooting
+
+- **Terraform plan still shows version diffs after `-refresh-only`**: confirm that the `azurerm_arc_kubernetes_cluster_extension` resources for `cert_manager`, `secret_store`, and `iot_operations` in state now show the upgraded `version`. If they do, the diff is being driven by code defaults newer than what `az iot ops upgrade` installed — running `terraform apply` will move Azure to those defaults.
+- **`az iot ops upgrade` reports no updates**: the cluster is already on the latest stable channel for all three components; nothing to reconcile.
+- **Permission errors during refresh**: ensure your principal has read access on the connected cluster, the cluster extensions, and the AIO instance resource group.
+
+## References
+
+- [Supported versions — Azure IoT Operations](https://learn.microsoft.com/azure/iot-operations/deploy-iot-ops/howto-upgrade?tabs=portal#supported-versions)
+- [Upgrade Azure IoT Operations — Official guide](https://learn.microsoft.com/azure/iot-operations/deploy-iot-ops/howto-upgrade)
diff --git a/src/000-cloud/030-data/terraform/modules/schema-registry/README.md b/src/000-cloud/030-data/terraform/modules/schema-registry/README.md
index bbf4a14d..3f6e420d 100644
--- a/src/000-cloud/030-data/terraform/modules/schema-registry/README.md
+++ b/src/000-cloud/030-data/terraform/modules/schema-registry/README.md
@@ -23,24 +23,27 @@ Storage Blob Data Contributor Role Assignment.
## Resources
-| Name | Type |
-|---------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
-| [azapi_resource.schema_registry](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource |
-| [azurerm_role_assignment.registry_storage_contributor](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
-| [azurerm_storage_container.schema_container](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container) | resource |
-| [terraform_data.defer](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
-| [time_sleep.wait_for_rbac_propagation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource |
+| Name | Type |
+|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
+| [azapi_resource.schema_registry](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource |
+| [azurerm_role_assignment.registry_storage_contributor](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
+| [azurerm_role_assignment.schema_container_blob_data_contributor](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
+| [azurerm_storage_container.schema_container](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container) | resource |
+| [terraform_data.defer](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
+| [time_sleep.wait_for_rbac_propagation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource |
+| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source |
## Inputs
-| Name | Description | Type | Default | Required |
-|------------------|-------------------------------------------------------------------------------|----------------------------------------------------------------------------|---------|:--------:|
-| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes |
-| instance | Instance identifier for naming resources: 001, 002, etc | `string` | n/a | yes |
-| location | Azure region where all resources will be deployed | `string` | n/a | yes |
-| resource\_group | Resource group object containing name and id where resources will be deployed | ```object({ id = string name = string })``` | n/a | yes |
-| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes |
-| storage\_account | n/a | ```object({ id = string name = string primary_blob_endpoint = string })``` | n/a | yes |
+| Name | Description | Type | Default | Required |
+|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|---------|:--------:|
+| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes |
+| instance | Instance identifier for naming resources: 001, 002, etc | `string` | n/a | yes |
+| location | Azure region where all resources will be deployed | `string` | n/a | yes |
+| resource\_group | Resource group object containing name and id where resources will be deployed | ```object({ id = string name = string })``` | n/a | yes |
+| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes |
+| storage\_account | n/a | ```object({ id = string name = string primary_blob_endpoint = string })``` | n/a | yes |
+| blob\_data\_contributor\_principal\_id | The principal ID that will be assigned the 'Storage Blob Data Contributor' role on the schemas container so it can upload schema versions. Defaults to the current Azure client when null. | `string` | `null` | no |
## Outputs
diff --git a/src/000-cloud/030-data/terraform/modules/schema-registry/main.tf b/src/000-cloud/030-data/terraform/modules/schema-registry/main.tf
index 04b30a98..201f15cf 100644
--- a/src/000-cloud/030-data/terraform/modules/schema-registry/main.tf
+++ b/src/000-cloud/030-data/terraform/modules/schema-registry/main.tf
@@ -57,10 +57,23 @@ resource "azurerm_role_assignment" "registry_storage_contributor" {
skip_service_principal_aad_check = true
}
+// Grant blob data contributor on the schemas container so schema versions can be uploaded.
+// Independent of the optional data-lake module so schema uploads work when should_create_data_lake = false.
+data "azurerm_client_config" "current" {}
+
+resource "azurerm_role_assignment" "schema_container_blob_data_contributor" {
+ principal_id = coalesce(var.blob_data_contributor_principal_id, data.azurerm_client_config.current.object_id)
+ role_definition_name = "Storage Blob Data Contributor"
+ scope = azurerm_storage_container.schema_container.id
+}
+
// Azure RBAC propagation delay for blob data-plane access.
resource "time_sleep" "wait_for_rbac_propagation" {
create_duration = "30s"
- depends_on = [azurerm_role_assignment.registry_storage_contributor]
+ depends_on = [
+ azurerm_role_assignment.registry_storage_contributor,
+ azurerm_role_assignment.schema_container_blob_data_contributor,
+ ]
}
resource "terraform_data" "defer" {
diff --git a/src/000-cloud/030-data/terraform/modules/schema-registry/variables.core.tf b/src/000-cloud/030-data/terraform/modules/schema-registry/variables.core.tf
index 3392bce0..ea1feee9 100644
--- a/src/000-cloud/030-data/terraform/modules/schema-registry/variables.core.tf
+++ b/src/000-cloud/030-data/terraform/modules/schema-registry/variables.core.tf
@@ -25,3 +25,9 @@ variable "instance" {
type = string
description = "Instance identifier for naming resources: 001, 002, etc"
}
+
+variable "blob_data_contributor_principal_id" {
+ type = string
+ description = "The principal ID that will be assigned the 'Storage Blob Data Contributor' role on the schemas container so it can upload schema versions. Defaults to the current Azure client when null."
+ default = null
+}
diff --git a/src/100-edge/100-cncf-cluster/scripts/deploy-script-secrets.sh b/src/100-edge/100-cncf-cluster/scripts/deploy-script-secrets.sh
index 85ef93d2..80d23fb4 100755
--- a/src/100-edge/100-cncf-cluster/scripts/deploy-script-secrets.sh
+++ b/src/100-edge/100-cncf-cluster/scripts/deploy-script-secrets.sh
@@ -125,7 +125,7 @@ fi
if [ -z "$SKIP_AZ_LOGIN" ]; then
if [ -n "$CLIENT_ID" ]; then
log "Logging in with User Assigned Managed Identity (client ID: $CLIENT_ID)"
- if ! az login --identity --client-id "$CLIENT_ID"; then
+ if ! az login --identity --username "$CLIENT_ID"; then
err "Failed to login with User Assigned Managed Identity (client ID: $CLIENT_ID)"
fi
else
diff --git a/src/100-edge/100-cncf-cluster/scripts/k3s-device-setup.sh b/src/100-edge/100-cncf-cluster/scripts/k3s-device-setup.sh
index e76ab605..e3ecf4cb 100755
--- a/src/100-edge/100-cncf-cluster/scripts/k3s-device-setup.sh
+++ b/src/100-edge/100-cncf-cluster/scripts/k3s-device-setup.sh
@@ -137,7 +137,7 @@ if [[ ! $SKIP_AZ_LOGIN ]]; then
else
if [[ $CLIENT_ID ]]; then
log "Logging into Azure CLI using managed identity client ID $CLIENT_ID"
- if ! az login --identity --client-id "$CLIENT_ID" --allow-no-subscriptions; then
+ if ! az login --identity --username "$CLIENT_ID" --allow-no-subscriptions; then
err "Azure CLI login failed for managed identity client ID $CLIENT_ID"
fi
else
diff --git a/src/100-edge/109-arc-extensions/bicep/types.bicep b/src/100-edge/109-arc-extensions/bicep/types.bicep
index 07005bed..622e4992 100644
--- a/src/100-edge/109-arc-extensions/bicep/types.bicep
+++ b/src/100-edge/109-arc-extensions/bicep/types.bicep
@@ -39,7 +39,7 @@ type CertManagerExtension = {
var certManagerExtensionDefaults = {
enabled: true
release: {
- version: '0.10.2'
+ version: '0.11.0'
train: 'stable'
autoUpgradeMinorVersion: false
}
diff --git a/src/100-edge/109-arc-extensions/terraform/README.md b/src/100-edge/109-arc-extensions/terraform/README.md
index c3b84fc7..42cf7bab 100644
--- a/src/100-edge/109-arc-extensions/terraform/README.md
+++ b/src/100-edge/109-arc-extensions/terraform/README.md
@@ -23,7 +23,7 @@ cert-manager and Azure Container Storage (ACSA).
| Name | Description | Type | Default | Required |
|-------------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
| arc\_connected\_cluster | Arc-connected Kubernetes cluster object containing id, name, and location | ```object({ id = string name = string location = string })``` | n/a | yes |
-| arc\_extensions | Combined configuration object for Arc extensions (cert-manager and container storage) | ```object({ cert_manager_extension = optional(object({ enabled = optional(bool) version = optional(string) train = optional(string) auto_upgrade_minor_version = optional(bool) agent_operation_timeout_in_minutes = optional(number) global_telemetry_enabled = optional(bool) })) container_storage_extension = optional(object({ enabled = optional(bool) version = optional(string) train = optional(string) auto_upgrade_minor_version = optional(bool) disk_storage_class = optional(string) fault_tolerance_enabled = optional(bool) disk_mount_point = optional(string) })) })``` | ```{ "cert_manager_extension": { "agent_operation_timeout_in_minutes": 20, "auto_upgrade_minor_version": false, "enabled": true, "global_telemetry_enabled": true, "train": "stable", "version": "0.10.2" }, "container_storage_extension": { "auto_upgrade_minor_version": false, "disk_mount_point": "/mnt", "disk_storage_class": "", "enabled": true, "fault_tolerance_enabled": false, "train": "stable", "version": "2.6.0" } }``` | no |
+| arc\_extensions | Combined configuration object for Arc extensions (cert-manager and container storage) | ```object({ cert_manager_extension = optional(object({ enabled = optional(bool) version = optional(string) train = optional(string) auto_upgrade_minor_version = optional(bool) agent_operation_timeout_in_minutes = optional(number) global_telemetry_enabled = optional(bool) })) container_storage_extension = optional(object({ enabled = optional(bool) version = optional(string) train = optional(string) auto_upgrade_minor_version = optional(bool) disk_storage_class = optional(string) fault_tolerance_enabled = optional(bool) disk_mount_point = optional(string) })) })``` | ```{ "cert_manager_extension": { "agent_operation_timeout_in_minutes": 20, "auto_upgrade_minor_version": false, "enabled": true, "global_telemetry_enabled": true, "train": "stable", "version": "0.11.0" }, "container_storage_extension": { "auto_upgrade_minor_version": false, "disk_mount_point": "/mnt", "disk_storage_class": "", "enabled": true, "fault_tolerance_enabled": false, "train": "stable", "version": "2.6.0" } }``` | no |
## Outputs
diff --git a/src/100-edge/109-arc-extensions/terraform/variables.tf b/src/100-edge/109-arc-extensions/terraform/variables.tf
index 81872e9f..ca030d31 100644
--- a/src/100-edge/109-arc-extensions/terraform/variables.tf
+++ b/src/100-edge/109-arc-extensions/terraform/variables.tf
@@ -22,7 +22,7 @@ variable "arc_extensions" {
default = {
cert_manager_extension = {
enabled = true
- version = "0.10.2"
+ version = "0.11.0"
train = "stable"
auto_upgrade_minor_version = false
agent_operation_timeout_in_minutes = 20
diff --git a/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep b/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep
index dbaf3c60..1612fffa 100644
--- a/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep
+++ b/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep
@@ -130,6 +130,7 @@ var defaultConfigurationSettings = {
'AgentOperationTimeoutInMinutes': any(aioExtensionConfig.settings.agentOperationTimeoutInMinutes)
'connectors.values.mqttBroker.address': aioMqBrokerAddress
'connectors.values.mqttBroker.serviceAccountTokenAudience': aioMqBrokerConfig.serviceAccountAudience
+ 'connectors.values.securityPki.applicationUri': 'urn:microsoft.com:aio:opc:ua:broker:${take(uniqueString(arcConnectedCluster.id), 5)}'
'dataFlows.values.tinyKube.mqttBroker.hostName': '${aioMqBrokerConfig.brokerListenerServiceName}.${aioExtensionConfig.settings.namespace}'
'dataFlows.values.tinyKube.mqttBroker.port': any(aioMqBrokerConfig.brokerListenerPort)
'dataFlows.values.tinyKube.mqttBroker.authentication.serviceAccountTokenAudience': aioMqBrokerConfig.serviceAccountAudience
diff --git a/src/100-edge/110-iot-ops/bicep/types.bicep b/src/100-edge/110-iot-ops/bicep/types.bicep
index 7c7d638f..5c16d49c 100644
--- a/src/100-edge/110-iot-ops/bicep/types.bicep
+++ b/src/100-edge/110-iot-ops/bicep/types.bicep
@@ -27,7 +27,7 @@ type SecretStoreExtension = {
@export()
var secretStoreExtensionDefaults = {
release: {
- version: '1.3.0'
+ version: '1.4.0'
train: 'stable'
}
}
@@ -53,7 +53,7 @@ type AioExtension = {
@export()
var aioExtensionDefaults = {
release: {
- version: '1.3.38'
+ version: '1.3.70'
train: 'stable'
}
settings: {
diff --git a/src/100-edge/110-iot-ops/terraform/README.md b/src/100-edge/110-iot-ops/terraform/README.md
index 87fa7885..1801545c 100644
--- a/src/100-edge/110-iot-ops/terraform/README.md
+++ b/src/100-edge/110-iot-ops/terraform/README.md
@@ -54,9 +54,9 @@ Instance can be created, and after.
| mqtt\_broker\_diagnostics\_config | Extended broker diagnostics configuration for metrics, self-check, and distributed tracing | ```object({ metrics = optional(object({ prometheus_port = optional(number) })) self_check = optional(object({ mode = optional(string) interval_seconds = optional(number) timeout_seconds = optional(number) })) traces = optional(object({ mode = optional(string) cache_size_megabytes = optional(number) span_channel_capacity = optional(number) self_tracing = optional(object({ mode = optional(string) interval_seconds = optional(number) })) })) })``` | `null` | no |
| mqtt\_broker\_disk\_buffer\_config | Disk-backed message buffer configuration for broker in-memory overflow to disk | ```object({ max_size = string ephemeral_volume_claim_spec = optional(object({ storage_class_name = optional(string) access_modes = optional(list(string)) volume_mode = optional(string) volume_name = optional(string) resources = optional(object({ requests = optional(map(string)) limits = optional(map(string)) })) data_source = optional(object({ api_group = optional(string) kind = string name = string })) data_source_ref = optional(object({ api_group = optional(string) kind = string name = string namespace = optional(string) })) selector = optional(object({ match_labels = optional(map(string)) match_expressions = optional(list(object({ key = string operator = string values = list(string) }))) })) })) persistent_volume_claim_spec = optional(object({ storage_class_name = optional(string) access_modes = optional(list(string)) volume_mode = optional(string) volume_name = optional(string) resources = optional(object({ requests = optional(map(string)) limits = optional(map(string)) })) data_source = optional(object({ api_group = optional(string) kind = string name = string })) data_source_ref = optional(object({ api_group = optional(string) kind = string name = string namespace = optional(string) })) selector = optional(object({ match_labels = optional(map(string)) match_expressions = optional(list(object({ key = string operator = string values = list(string) }))) })) })) })``` | `null` | no |
| mqtt\_broker\_persistence\_config | Broker persistence configuration for disk-backed message storage | ```object({ max_size = string encryption_enabled = optional(bool) # Retention Policy retain_policy = optional(object({ mode = string # "All", "None", "Custom" custom_settings = optional(object({ topics = optional(list(string)) dynamic_enabled = optional(bool) })) })) # State Store Policy state_store_policy = optional(object({ mode = string # "All", "None", "Custom" custom_settings = optional(object({ state_store_resources = optional(list(object({ key_type = string # "Pattern", "String", "Binary" keys = list(string) }))) dynamic_enabled = optional(bool) })) })) # Subscriber Queue Policy subscriber_queue_policy = optional(object({ mode = string # "All", "None", "Custom" custom_settings = optional(object({ subscriber_client_ids = optional(list(string)) dynamic_enabled = optional(bool) })) })) # Persistent Volume Claim Specification persistent_volume_claim_spec = optional(object({ storage_class_name = optional(string) access_modes = optional(list(string)) volume_mode = optional(string) volume_name = optional(string) resources = optional(object({ requests = optional(map(string)) limits = optional(map(string)) })) data_source = optional(object({ api_group = optional(string) kind = string name = string })) data_source_ref = optional(object({ api_group = optional(string) kind = string name = string namespace = optional(string) })) selector = optional(object({ match_labels = optional(map(string)) match_expressions = optional(list(object({ key = string operator = string values = list(string) }))) })) })) })``` | `null` | no |
-| operations\_config | n/a | ```object({ namespace = string kubernetesDistro = string version = string train = string agentOperationTimeoutInMinutes = number })``` | ```{ "agentOperationTimeoutInMinutes": 120, "kubernetesDistro": "K3s", "namespace": "azure-iot-operations", "train": "stable", "version": "1.3.38" }``` | no |
+| operations\_config | n/a | ```object({ namespace = string kubernetesDistro = string version = string train = string agentOperationTimeoutInMinutes = number })``` | ```{ "agentOperationTimeoutInMinutes": 120, "kubernetesDistro": "K3s", "namespace": "azure-iot-operations", "train": "stable", "version": "1.3.70" }``` | no |
| registry\_endpoints | List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. | ```list(object({ name = string host = string acr_resource_id = optional(string) should_assign_acr_pull_for_aio = optional(bool, false) authentication = object({ method = string system_assigned_managed_identity_settings = optional(object({ audience = optional(string, "https://management.azure.com/") })) user_assigned_managed_identity_settings = optional(object({ client_id = string tenant_id = string scope = optional(string) })) artifact_pull_secret_settings = optional(object({ secret_ref = string })) }) }))``` | `[]` | no |
-| secret\_sync\_controller | n/a | ```object({ version = string train = string })``` | ```{ "train": "stable", "version": "1.3.0" }``` | no |
+| secret\_sync\_controller | n/a | ```object({ version = string train = string })``` | ```{ "train": "stable", "version": "1.4.0" }``` | no |
| should\_assign\_key\_vault\_roles | Whether to assign Key Vault roles to provided Secret Sync identity. | `bool` | `true` | no |
| should\_create\_anonymous\_broker\_listener | Whether to enable an insecure anonymous AIO MQ Broker Listener. Should only be used for dev or test environments | `bool` | `false` | no |
| should\_deploy\_resource\_sync\_rules | Deploys resource sync rules if set to true | `bool` | `false` | no |
diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf
index 60f4afea..33d8cb39 100644
--- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf
+++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf
@@ -42,6 +42,7 @@ locals {
"AgentOperationTimeoutInMinutes" = tostring(var.operations_config.agentOperationTimeoutInMinutes)
"connectors.values.mqttBroker.address" = local.mqtt_broker_address
"connectors.values.mqttBroker.serviceAccountTokenAudience" = var.mqtt_broker_config.serviceAccountAudience
+ "connectors.values.securityPki.applicationUri" = "urn:microsoft.com:aio:opc:ua:broker:${substr(sha256(var.arc_connected_cluster_id), 0, 5)}"
"dataFlows.values.tinyKube.mqttBroker.hostName" = local.mqtt_broker_hostname
"dataFlows.values.tinyKube.mqttBroker.port" = tostring(var.mqtt_broker_config.brokerListenerPort)
"dataFlows.values.tinyKube.mqttBroker.authentication.serviceAccountTokenAudience" = var.mqtt_broker_config.serviceAccountAudience
diff --git a/src/100-edge/110-iot-ops/terraform/variables.init.tf b/src/100-edge/110-iot-ops/terraform/variables.init.tf
index 81370fa8..898a7430 100644
--- a/src/100-edge/110-iot-ops/terraform/variables.init.tf
+++ b/src/100-edge/110-iot-ops/terraform/variables.init.tf
@@ -13,7 +13,7 @@ variable "secret_sync_controller" {
train = string
})
default = {
- version = "1.3.0"
+ version = "1.4.0"
train = "stable"
}
}
diff --git a/src/100-edge/110-iot-ops/terraform/variables.instance.tf b/src/100-edge/110-iot-ops/terraform/variables.instance.tf
index 688feef3..06573f42 100644
--- a/src/100-edge/110-iot-ops/terraform/variables.instance.tf
+++ b/src/100-edge/110-iot-ops/terraform/variables.instance.tf
@@ -18,7 +18,7 @@ variable "operations_config" {
default = {
namespace = "azure-iot-operations"
kubernetesDistro = "K3s"
- version = "1.3.38"
+ version = "1.3.70"
train = "stable"
agentOperationTimeoutInMinutes = 120
}