Path to ontology definition YAML file (required)
+
+OPTIONS:
+ -v, --verbose Enable verbose output
+ -h, --help Show this help message
+
+EXAMPLES:
+ # Validate the Lakeshore Retail example
+ ./validate-definition.sh -d definitions/examples/lakeshore-retail.yaml
+
+ # Validate with verbose output
+ ./validate-definition.sh -d my-ontology.yaml --verbose
+
+EXIT CODES:
+ 0 - Definition is valid
+ 1 - Validation failed
+ 2 - Invalid arguments or missing dependencies
+EOF
+}
+
+#===============================================================================
+# Argument Parsing
+#===============================================================================
+DEFINITION_FILE=""
+
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -d|--definition)
+ DEFINITION_FILE="$2"
+ shift 2
+ ;;
+ -v|--verbose)
+ VERBOSE=true
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ err "Unknown argument: $1"
+ usage
+ exit 2
+ ;;
+ esac
+ done
+
+ if [[ -z "$DEFINITION_FILE" ]]; then
+ err "Missing required argument: --definition"
+ usage
+ exit 2
+ fi
+
+ if [[ ! -f "$DEFINITION_FILE" ]]; then
+ err "Definition file not found: $DEFINITION_FILE"
+ exit 2
+ fi
+}
+
+#===============================================================================
+# Validation Functions
+#===============================================================================
+ERRORS=()
+WARNINGS=()
+
+add_error() {
+ ERRORS+=("$1")
+ err "$1"
+}
+
+add_warning() {
+ WARNINGS+=("$1")
+ warn "$1"
+}
+
+# Check if value is in array
+in_array() {
+ local needle="$1"
+ shift
+ local item
+ for item in "$@"; do
+ [[ "$item" == "$needle" ]] && return 0
+ done
+ return 1
+}
+
+#-------------------------------------------------------------------------------
+# Validate API version and kind
+#-------------------------------------------------------------------------------
+validate_api_version() {
+ debug "Checking apiVersion and kind..."
+
+ local api_version
+ api_version=$(yq -r '.apiVersion // ""' "$DEFINITION_FILE")
+
+ if [[ -z "$api_version" ]]; then
+ add_error "Missing required field: apiVersion"
+ elif [[ "$api_version" != "fabric.ontology/v1" ]]; then
+ add_error "Invalid apiVersion: '$api_version' (expected 'fabric.ontology/v1')"
+ fi
+
+ local kind
+ kind=$(yq -r '.kind // ""' "$DEFINITION_FILE")
+
+ if [[ -z "$kind" ]]; then
+ add_error "Missing required field: kind"
+ elif [[ "$kind" != "OntologyDefinition" ]]; then
+ add_error "Invalid kind: '$kind' (expected 'OntologyDefinition')"
+ fi
+}
+
+#-------------------------------------------------------------------------------
+# Validate metadata section
+#-------------------------------------------------------------------------------
+validate_metadata() {
+ debug "Checking metadata..."
+
+ local name
+ name=$(get_metadata_name "$DEFINITION_FILE")
+
+ if [[ -z "$name" || "$name" == "null" ]]; then
+ add_error "Missing required field: metadata.name"
+ else
+ debug " metadata.name: $name"
+ fi
+}
+
+#-------------------------------------------------------------------------------
+# Validate entity types
+#-------------------------------------------------------------------------------
+validate_entity_types() {
+ debug "Checking entityTypes..."
+
+ local count
+ count=$(get_entity_type_count "$DEFINITION_FILE")
+
+ if [[ "$count" -eq 0 ]]; then
+ add_error "At least one entityType is required"
+ return
+ fi
+
+ debug " Found $count entity type(s)"
+
+ # Collect all entity names for relationship validation
+ local entity_names=()
+ while IFS= read -r name; do
+ entity_names+=("$name")
+ done < <(get_entity_type_names "$DEFINITION_FILE")
+
+ # Validate each entity type
+ for entity_name in "${entity_names[@]}"; do
+ validate_entity_type "$entity_name"
+ done
+}
+
+validate_entity_type() {
+ local entity_name="$1"
+ debug " Validating entity: $entity_name"
+
+ # Get entity key
+ local key
+ key=$(get_entity_key "$DEFINITION_FILE" "$entity_name")
+
+ if [[ -z "$key" || "$key" == "null" ]]; then
+ add_error "Entity '$entity_name': Missing required field 'key'"
+ return
+ fi
+
+ # Get property names
+ local prop_names=()
+ while IFS= read -r prop_name; do
+ prop_names+=("$prop_name")
+ done < <(get_entity_property_names "$DEFINITION_FILE" "$entity_name")
+
+ if [[ ${#prop_names[@]} -eq 0 ]]; then
+ add_error "Entity '$entity_name': At least one property is required"
+ return
+ fi
+
+ # Validate key references a valid property
+ if ! in_array "$key" "${prop_names[@]}"; then
+ add_error "Entity '$entity_name': Key '$key' does not reference a valid property. Available: ${prop_names[*]}"
+ fi
+
+ # Validate each property
+ local properties
+ properties=$(get_entity_properties "$DEFINITION_FILE" "$entity_name")
+
+ echo "$properties" | jq -c '.[]' | while read -r prop; do
+ local prop_name prop_type prop_binding
+ prop_name=$(echo "$prop" | jq -r '.name')
+ prop_type=$(echo "$prop" | jq -r '.type')
+ prop_binding=$(echo "$prop" | jq -r '.binding // "static"')
+
+ # Validate property type
+ if ! in_array "$prop_type" "${SUPPORTED_TYPES[@]}"; then
+ add_error "Entity '$entity_name', property '$prop_name': Invalid type '$prop_type'. Supported: ${SUPPORTED_TYPES[*]}"
+ fi
+
+ # Validate binding type if specified
+ if [[ "$prop_binding" != "null" ]] && ! in_array "$prop_binding" "${SUPPORTED_BINDINGS[@]}"; then
+ add_error "Entity '$entity_name', property '$prop_name': Invalid binding '$prop_binding'. Supported: ${SUPPORTED_BINDINGS[*]}"
+ fi
+ done
+
+ # Validate data bindings
+ validate_entity_bindings "$entity_name"
+}
+
+validate_entity_bindings() {
+ local entity_name="$1"
+
+ # Check for single dataBinding
+ local single_binding
+ single_binding=$(get_entity_data_binding "$DEFINITION_FILE" "$entity_name")
+
+ # Check for multiple dataBindings
+ local multi_bindings
+ multi_bindings=$(get_entity_data_bindings "$DEFINITION_FILE" "$entity_name")
+
+ local has_single has_multi
+ has_single=$([[ "$single_binding" != "null" && -n "$single_binding" ]] && echo "true" || echo "false")
+ has_multi=$([[ $(echo "$multi_bindings" | jq 'length') -gt 0 ]] && echo "true" || echo "false")
+
+ if [[ "$has_single" == "false" && "$has_multi" == "false" ]]; then
+ add_warning "Entity '$entity_name': No dataBinding or dataBindings defined"
+ return
+ fi
+
+ # Validate single binding
+ if [[ "$has_single" == "true" ]]; then
+ validate_binding "$entity_name" "$single_binding" "dataBinding"
+ fi
+
+ # Validate multiple bindings
+ if [[ "$has_multi" == "true" ]]; then
+ echo "$multi_bindings" | jq -c '.[]' | while read -r binding; do
+ local binding_type
+ binding_type=$(echo "$binding" | jq -r '.type')
+ validate_binding "$entity_name" "$binding" "dataBindings[$binding_type]"
+ done
+ fi
+}
+
+validate_binding() {
+ local entity_name="$1"
+ local binding="$2"
+ local binding_path="$3"
+
+ local binding_type source table
+ binding_type=$(echo "$binding" | jq -r '.type')
+ source=$(echo "$binding" | jq -r '.source')
+ table=$(echo "$binding" | jq -r '.table')
+
+ # Validate binding type
+ if ! in_array "$binding_type" "${SUPPORTED_BINDINGS[@]}"; then
+ add_error "Entity '$entity_name', $binding_path: Invalid type '$binding_type'. Supported: ${SUPPORTED_BINDINGS[*]}"
+ fi
+
+ # Validate source
+ if ! in_array "$source" "${SUPPORTED_SOURCES[@]}"; then
+ add_error "Entity '$entity_name', $binding_path: Invalid source '$source'. Supported: ${SUPPORTED_SOURCES[*]}"
+ fi
+
+ # Validate table is specified
+ if [[ -z "$table" || "$table" == "null" ]]; then
+ add_error "Entity '$entity_name', $binding_path: Missing required field 'table'"
+ fi
+
+ # Validate source is defined in dataSources
+ if [[ "$source" == "lakehouse" ]]; then
+ local lakehouse_name
+ lakehouse_name=$(get_lakehouse_name "$DEFINITION_FILE")
+ if [[ -z "$lakehouse_name" || "$lakehouse_name" == "null" ]]; then
+ add_error "Entity '$entity_name', $binding_path: References lakehouse but dataSources.lakehouse is not defined"
+ else
+ # Validate table exists in lakehouse
+ local table_exists
+ table_exists=$(yq ".dataSources.lakehouse.tables[] | select(.name == \"$table\") | .name" "$DEFINITION_FILE")
+ if [[ -z "$table_exists" ]]; then
+ add_error "Entity '$entity_name', $binding_path: Table '$table' not found in dataSources.lakehouse.tables"
+ fi
+ fi
+ elif [[ "$source" == "eventhouse" ]]; then
+ local eventhouse_name
+ eventhouse_name=$(get_eventhouse_name "$DEFINITION_FILE")
+ if [[ -z "$eventhouse_name" || "$eventhouse_name" == "null" ]]; then
+ add_error "Entity '$entity_name', $binding_path: References eventhouse but dataSources.eventhouse is not defined"
+ else
+ # Validate table exists in eventhouse
+ local table_exists
+ table_exists=$(yq ".dataSources.eventhouse.tables[] | select(.name == \"$table\") | .name" "$DEFINITION_FILE")
+ if [[ -z "$table_exists" ]]; then
+ add_error "Entity '$entity_name', $binding_path: Table '$table' not found in dataSources.eventhouse.tables"
+ fi
+ fi
+ fi
+
+ # Validate timeseries-specific fields
+ if [[ "$binding_type" == "timeseries" ]]; then
+ local timestamp_col
+ timestamp_col=$(echo "$binding" | jq -r '.timestampColumn // ""')
+ if [[ -z "$timestamp_col" ]]; then
+ add_error "Entity '$entity_name', $binding_path: Timeseries binding requires 'timestampColumn'"
+ fi
+ fi
+}
+
+#-------------------------------------------------------------------------------
+# Validate relationships
+#-------------------------------------------------------------------------------
+validate_relationships() {
+ debug "Checking relationships..."
+
+ local count
+ count=$(get_relationship_count "$DEFINITION_FILE")
+
+ if [[ "$count" -eq 0 ]]; then
+ debug " No relationships defined (optional)"
+ return
+ fi
+
+ debug " Found $count relationship(s)"
+
+ # Collect all entity names
+ local entity_names=()
+ while IFS= read -r name; do
+ entity_names+=("$name")
+ done < <(get_entity_type_names "$DEFINITION_FILE")
+
+ # Validate each relationship
+ while IFS= read -r rel_name; do
+ validate_relationship "$rel_name" "${entity_names[@]}"
+ done < <(get_relationship_names "$DEFINITION_FILE")
+}
+
+validate_relationship() {
+ local rel_name="$1"
+ shift
+ local entity_names=("$@")
+
+ debug " Validating relationship: $rel_name"
+
+ local rel
+ rel=$(get_relationship "$DEFINITION_FILE" "$rel_name")
+
+ local from_entity to_entity
+ from_entity=$(echo "$rel" | jq -r '.from')
+ to_entity=$(echo "$rel" | jq -r '.to')
+
+ # Validate from entity exists
+ if ! in_array "$from_entity" "${entity_names[@]}"; then
+ add_error "Relationship '$rel_name': 'from' entity '$from_entity' not found. Available: ${entity_names[*]}"
+ fi
+
+ # Validate to entity exists
+ if ! in_array "$to_entity" "${entity_names[@]}"; then
+ add_error "Relationship '$rel_name': 'to' entity '$to_entity' not found. Available: ${entity_names[*]}"
+ fi
+}
+
+#-------------------------------------------------------------------------------
+# Validate data sources
+#-------------------------------------------------------------------------------
+validate_data_sources() {
+ debug "Checking dataSources..."
+
+ local has_lakehouse has_eventhouse
+ has_lakehouse=$(has_lakehouse "$DEFINITION_FILE" && echo "true" || echo "false")
+ has_eventhouse=$(has_eventhouse "$DEFINITION_FILE" && echo "true" || echo "false")
+
+ if [[ "$has_lakehouse" == "false" && "$has_eventhouse" == "false" ]]; then
+ add_warning "No data sources defined (dataSources.lakehouse or dataSources.eventhouse)"
+ fi
+
+ if [[ "$has_lakehouse" == "true" ]]; then
+ validate_lakehouse_config
+ fi
+
+ if [[ "$has_eventhouse" == "true" ]]; then
+ validate_eventhouse_config
+ fi
+}
+
+validate_lakehouse_config() {
+ debug " Validating lakehouse configuration..."
+
+ local name
+ name=$(get_lakehouse_name "$DEFINITION_FILE")
+ debug " name: $name"
+
+ local tables
+ tables=$(get_lakehouse_tables "$DEFINITION_FILE")
+ local table_count
+ table_count=$(echo "$tables" | jq 'length')
+
+ if [[ "$table_count" -eq 0 ]]; then
+ add_error "dataSources.lakehouse: At least one table is required"
+ fi
+
+ # Validate each table has name
+ echo "$tables" | jq -c '.[]' | while read -r table; do
+ local table_name
+ table_name=$(echo "$table" | jq -r '.name // ""')
+ if [[ -z "$table_name" ]]; then
+ add_error "dataSources.lakehouse.tables: Table missing required field 'name'"
+ fi
+ done
+}
+
+validate_eventhouse_config() {
+ debug " Validating eventhouse configuration..."
+
+ local name database
+ name=$(get_eventhouse_name "$DEFINITION_FILE")
+ database=$(get_eventhouse_database "$DEFINITION_FILE")
+
+ debug " name: $name"
+ debug " database: $database"
+
+ if [[ -z "$database" || "$database" == "null" ]]; then
+ add_error "dataSources.eventhouse: Missing required field 'database'"
+ fi
+
+ local tables
+ tables=$(get_eventhouse_tables "$DEFINITION_FILE")
+ local table_count
+ table_count=$(echo "$tables" | jq 'length')
+
+ if [[ "$table_count" -eq 0 ]]; then
+ add_error "dataSources.eventhouse: At least one table is required"
+ fi
+
+ # Validate each table has name and schema
+ echo "$tables" | jq -c '.[]' | while read -r table; do
+ local table_name schema_count
+ table_name=$(echo "$table" | jq -r '.name // ""')
+ schema_count=$(echo "$table" | jq '.schema | length // 0')
+
+ if [[ -z "$table_name" ]]; then
+ add_error "dataSources.eventhouse.tables: Table missing required field 'name'"
+ elif [[ "$schema_count" -eq 0 ]]; then
+ add_error "dataSources.eventhouse.tables[$table_name]: Missing required field 'schema'"
+ fi
+ done
+}
+
+#===============================================================================
+# Main
+#===============================================================================
+main() {
+ parse_args "$@"
+
+ log "Validating definition: $DEFINITION_FILE"
+ echo
+
+ # Run all validations
+ validate_api_version
+ validate_metadata
+ validate_data_sources
+ validate_entity_types
+ validate_relationships
+
+ echo
+
+ # Summary
+ local error_count=${#ERRORS[@]}
+ local warning_count=${#WARNINGS[@]}
+
+ if [[ $error_count -eq 0 ]]; then
+ success "Definition is valid"
+ if [[ $warning_count -gt 0 ]]; then
+ log "$warning_count warning(s)"
+ fi
+ exit 0
+ else
+ err "Validation failed with $error_count error(s)"
+ if [[ $warning_count -gt 0 ]]; then
+ log "$warning_count warning(s)"
+ fi
+ exit 1
+ fi
+}
+
+main "$@"
diff --git a/src/000-cloud/033-fabric-ontology/templates/kql/create-mapping.kql.tmpl b/src/000-cloud/033-fabric-ontology/templates/kql/create-mapping.kql.tmpl
new file mode 100644
index 00000000..6821a9df
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/kql/create-mapping.kql.tmpl
@@ -0,0 +1,18 @@
+// KQL CSV Ingestion Mapping Template
+// Generates .create-or-alter table ingestion csv mapping command
+//
+// Required Variables (set via envsubst):
+// TABLE_NAME - Name of the table
+// MAPPING_NAME - Name for the mapping (typically "${TABLE_NAME}CsvMapping")
+// MAPPING_JSON - JSON array of column mappings with Name, DataType, Ordinal
+//
+// Usage:
+// export TABLE_NAME="FreezerTelemetry"
+// export MAPPING_NAME="FreezerTelemetryCsvMapping"
+// export MAPPING_JSON='[{"Name":"timestamp","DataType":"datetime","Ordinal":0},{"Name":"storeId","DataType":"string","Ordinal":1}]'
+// envsubst < create-mapping.kql.tmpl
+//
+// Output Example:
+// .create-or-alter table FreezerTelemetry ingestion csv mapping 'FreezerTelemetryCsvMapping' '[...]'
+
+.create-or-alter table ${TABLE_NAME} ingestion csv mapping '${MAPPING_NAME}' '${MAPPING_JSON}'
diff --git a/src/000-cloud/033-fabric-ontology/templates/kql/create-table.kql.tmpl b/src/000-cloud/033-fabric-ontology/templates/kql/create-table.kql.tmpl
new file mode 100644
index 00000000..c33955eb
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/kql/create-table.kql.tmpl
@@ -0,0 +1,16 @@
+// KQL Table Creation Template
+// Generates .create-merge table command from placeholder variables
+//
+// Required Variables (set via envsubst):
+// TABLE_NAME - Name of the table to create
+// COLUMN_SCHEMA - Comma-separated column definitions (e.g., "AssetId: string, Timestamp: datetime")
+//
+// Usage:
+// export TABLE_NAME="FreezerTelemetry"
+// export COLUMN_SCHEMA="timestamp: datetime, storeId: string, freezerId: string, temperatureC: real"
+// envsubst < create-table.kql.tmpl
+//
+// Output Example:
+// .create-merge table FreezerTelemetry (timestamp: datetime, storeId: string, freezerId: string, temperatureC: real)
+
+.create-merge table ${TABLE_NAME} (${COLUMN_SCHEMA})
diff --git a/src/000-cloud/033-fabric-ontology/templates/kql/retention-policy.kql.tmpl b/src/000-cloud/033-fabric-ontology/templates/kql/retention-policy.kql.tmpl
new file mode 100644
index 00000000..b0ba693c
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/kql/retention-policy.kql.tmpl
@@ -0,0 +1,27 @@
+// KQL Retention and Caching Policy Template
+// Generates .alter table policy commands for retention and caching
+//
+// Required Variables (set via envsubst):
+// TABLE_NAME - Name of the table
+//
+// Optional Variables:
+// RETENTION_DAYS - Data retention period in days (default: 30)
+// CACHING_DAYS - Hot cache period in days (default: 7)
+//
+// Usage:
+// export TABLE_NAME="FreezerTelemetry"
+// export RETENTION_DAYS="30"
+// export CACHING_DAYS="7"
+// envsubst < retention-policy.kql.tmpl
+//
+// Output Example:
+// .alter table FreezerTelemetry policy retention @'{"SoftDeletePeriod":"30.00:00:00","Recoverability":"Enabled"}'
+// .alter table FreezerTelemetry policy caching hot = 7d
+//
+// Notes:
+// - SoftDeletePeriod format: "days.hours:minutes:seconds"
+// - Caching hot period uses shorthand format: Nd (days)
+// - Recoverability is enabled by default for data recovery
+
+.alter table ${TABLE_NAME} policy retention @'{"SoftDeletePeriod":"${RETENTION_DAYS}.00:00:00","Recoverability":"Enabled"}'
+.alter table ${TABLE_NAME} policy caching hot = ${CACHING_DAYS}d
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/contextualization.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/contextualization.json.tmpl
new file mode 100644
index 00000000..19c51b6a
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/contextualization.json.tmpl
@@ -0,0 +1,12 @@
+{
+ "id": "{{ .ContextualizationId }}",
+ "dataBindingTable": {
+ "workspaceId": "{{ .WorkspaceId }}",
+ "itemId": "{{ .LakehouseId }}",
+ "sourceTableName": "{{ .TableName }}",
+ "sourceSchema": "dbo",
+ "sourceType": "LakehouseTable"
+ },
+ "sourceKeyRefBindings": {{ .SourceKeyRefBindings }},
+ "targetKeyRefBindings": {{ .TargetKeyRefBindings }}
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/data-binding-eventhouse.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/data-binding-eventhouse.json.tmpl
new file mode 100644
index 00000000..a4771b3c
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/data-binding-eventhouse.json.tmpl
@@ -0,0 +1,16 @@
+{
+ "id": "{{ .BindingId }}",
+ "dataBindingConfiguration": {
+ "dataBindingType": "TimeSeries",
+ "timestampColumnName": "{{ .TimestampColumn }}",
+ "propertyBindings": {{ .PropertyBindings }},
+ "sourceTableProperties": {
+ "sourceType": "KustoTable",
+ "workspaceId": "{{ .WorkspaceId }}",
+ "itemId": "{{ .EventhouseId }}",
+ "clusterUri": "{{ .ClusterUri }}",
+ "databaseName": "{{ .DatabaseName }}",
+ "sourceTableName": "{{ .TableName }}"
+ }
+ }
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/data-binding-lakehouse.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/data-binding-lakehouse.json.tmpl
new file mode 100644
index 00000000..8257c737
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/data-binding-lakehouse.json.tmpl
@@ -0,0 +1,14 @@
+{
+ "id": "{{ .BindingId }}",
+ "dataBindingConfiguration": {
+ "dataBindingType": "NonTimeSeries",
+ "propertyBindings": {{ .PropertyBindings }},
+ "sourceTableProperties": {
+ "sourceType": "LakehouseTable",
+ "workspaceId": "{{ .WorkspaceId }}",
+ "itemId": "{{ .LakehouseId }}",
+ "sourceTableName": "{{ .TableName }}",
+ "sourceSchema": "dbo"
+ }
+ }
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/definition.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/definition.json.tmpl
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/definition.json.tmpl
@@ -0,0 +1 @@
+{}
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/entity-type.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/entity-type.json.tmpl
new file mode 100644
index 00000000..23f4e35d
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/entity-type.json.tmpl
@@ -0,0 +1,12 @@
+{
+ "id": "{{ .EntityId }}",
+ "namespace": "usertypes",
+ "baseEntityTypeId": null,
+ "name": "{{ .EntityName }}",
+ "entityIdParts": {{ .EntityIdParts }},
+ "displayNamePropertyId": "{{ .DisplayNamePropertyId }}",
+ "namespaceType": "Custom",
+ "visibility": "Visible",
+ "properties": {{ .Properties }},
+ "timeseriesProperties": {{ .TimeseriesProperties }}
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/platform.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/platform.json.tmpl
new file mode 100644
index 00000000..bdad588f
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/platform.json.tmpl
@@ -0,0 +1,6 @@
+{
+ "metadata": {
+ "type": "Ontology",
+ "displayName": "{{ .OntologyName }}"
+ }
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/ontology/relationship-type.json.tmpl b/src/000-cloud/033-fabric-ontology/templates/ontology/relationship-type.json.tmpl
new file mode 100644
index 00000000..3fabf763
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/ontology/relationship-type.json.tmpl
@@ -0,0 +1,12 @@
+{
+ "id": "{{ .RelationshipId }}",
+ "namespace": "usertypes",
+ "name": "{{ .RelationshipName }}",
+ "namespaceType": "Custom",
+ "source": {
+ "entityTypeId": "{{ .SourceEntityId }}"
+ },
+ "target": {
+ "entityTypeId": "{{ .TargetEntityId }}"
+ }
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/semantic-model/database.tmdl.tmpl b/src/000-cloud/033-fabric-ontology/templates/semantic-model/database.tmdl.tmpl
new file mode 100644
index 00000000..0ea4a4f5
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/semantic-model/database.tmdl.tmpl
@@ -0,0 +1,2 @@
+database '${MODEL_NAME}'
+ compatibilityLevel: 1604
diff --git a/src/000-cloud/033-fabric-ontology/templates/semantic-model/definition.pbism.tmpl b/src/000-cloud/033-fabric-ontology/templates/semantic-model/definition.pbism.tmpl
new file mode 100644
index 00000000..8e62e44b
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/semantic-model/definition.pbism.tmpl
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/semanticModel/definitionProperties/1.0.0/schema.json",
+ "version": "4.0"
+}
diff --git a/src/000-cloud/033-fabric-ontology/templates/semantic-model/expressions.tmdl.tmpl b/src/000-cloud/033-fabric-ontology/templates/semantic-model/expressions.tmdl.tmpl
new file mode 100644
index 00000000..6078c4c0
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/semantic-model/expressions.tmdl.tmpl
@@ -0,0 +1,3 @@
+expression DatabaseQuery =
+ AzureStorage.DataLake("https://onelake.dfs.fabric.microsoft.com/${WORKSPACE_ID}/${LAKEHOUSE_ID}")
+ meta [IsParameterQuery=false]
diff --git a/src/000-cloud/033-fabric-ontology/templates/semantic-model/model.tmdl.tmpl b/src/000-cloud/033-fabric-ontology/templates/semantic-model/model.tmdl.tmpl
new file mode 100644
index 00000000..562ddb7d
--- /dev/null
+++ b/src/000-cloud/033-fabric-ontology/templates/semantic-model/model.tmdl.tmpl
@@ -0,0 +1,5 @@
+model Model
+ culture: en-US
+ defaultPowerBIDataSourceVersion: powerBI_V3
+
+${TABLE_REFS}
diff --git a/src/000-cloud/055-vpn-gateway/bicep/README.md b/src/000-cloud/055-vpn-gateway/bicep/README.md
index fdfc79a8..2c0f8cdb 100644
--- a/src/000-cloud/055-vpn-gateway/bicep/README.md
+++ b/src/000-cloud/055-vpn-gateway/bicep/README.md
@@ -3,7 +3,7 @@
# VPN Gateway Component
-Creates a VPN Gateway with Point-to-Site and optional Site-to-Site connectivity.
+Creates a VPN Gateway with Point-to-Site and optional Site-to-Site connectivity.
Ths component currently only supports Azure AD (Entra ID) authentication for Point-to-Site VPN connections.
## Parameters
diff --git a/src/100-edge/100-cncf-cluster/bicep/README.md b/src/100-edge/100-cncf-cluster/bicep/README.md
index 68437673..0299aa80 100644
--- a/src/100-edge/100-cncf-cluster/bicep/README.md
+++ b/src/100-edge/100-cncf-cluster/bicep/README.md
@@ -8,35 +8,35 @@ The scripts handle primary and secondary node(s) setup, cluster administration,
## Parameters
-| Name | Description | Type | Default | Required |
-|:---------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------|:---------|
-| common | The common component configuration. | `[_1.Common](#user-defined-types)` | n/a | yes |
-| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | [format('arck-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no |
-| arcOnboardingSpClientId | Service Principal Client ID with Kubernetes Cluster - Azure Arc Onboarding permissions. | `string` | n/a | no |
-| arcOnboardingSpClientSecret | The Service Principal Client Secret for Arc onboarding. | `securestring` | n/a | no |
-| arcOnboardingSpPrincipalId | Service Principal Object Id used when assigning roles for Arc onboarding. | `string` | n/a | no |
-| arcOnboardingIdentityName | The resource name for the identity used for Arc onboarding. | `string` | n/a | 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 |
-| shouldAddCurrentUserClusterAdmin | Whether to add the current user as a cluster admin. | `bool` | `true` | no |
-| shouldEnableArcAutoUpgrade | Whether to enable auto-upgrade for Azure Arc agents. | `bool` | [not(equals(parameters('common').environment, 'prod'))] | no |
-| clusterAdminOid | The Object ID that will be given cluster-admin permissions. | `string` | n/a | no |
-| clusterAdminUpn | The User Principal Name that will be given cluster-admin permissions. | `string` | n/a | no |
-| clusterNodeVirtualMachineNames | The node virtual machines names. | `array` | n/a | no |
-| clusterServerVirtualMachineName | The server virtual machines name. | `string` | n/a | no |
-| clusterServerHostMachineUsername | Username used for the host machines that will be given kube-config settings on setup. (Otherwise, resource_prefix if it exists as a user) | `string` | [parameters('common').resourcePrefix] | no |
-| clusterServerIp | The IP address for the server for the cluster. (Needed for mult-node cluster) | `string` | n/a | no |
-| serverToken | The token that will be given to the server for the cluster or used by agent nodes. | `securestring` | n/a | no |
-| shouldAssignRoles | Whether to assign roles for Arc Onboarding. | `bool` | `true` | no |
-| shouldDeployScriptToVm | Whether to deploy the scripts to the VM. | `bool` | `true` | no |
-| shouldSkipInstallingAzCli | Should skip downloading and installing Azure CLI on the server. | `bool` | `false` | no |
-| shouldSkipAzCliLogin | Should skip login process with Azure CLI on the server. | `bool` | `false` | no |
-| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | deploy-user-token | no |
-| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | n/a | yes |
-| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [resourceGroup().name] | no |
-| k3sTokenSecretName | The name for the K3s token secret in Key Vault. | `string` | k3s-server-token | no |
-| nodeScriptSecretName | The name for the node script secret in Key Vault. | `string` | cluster-node-ubuntu-k3s | no |
-| serverScriptSecretName | The name for the server script secret in Key Vault. | `string` | cluster-server-ubuntu-k3s | no |
-| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no |
+| Name | Description | Type | Default | Required |
+|:---------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------|:---------|
+| common | The common component configuration. | `[_1.Common](#user-defined-types)` | n/a | yes |
+| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | [format('arck-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no |
+| arcOnboardingSpClientId | Service Principal Client ID with Kubernetes Cluster - Azure Arc Onboarding permissions. | `string` | n/a | no |
+| arcOnboardingSpClientSecret | The Service Principal Client Secret for Arc onboarding. | `securestring` | n/a | no |
+| arcOnboardingSpPrincipalId | Service Principal Object Id used when assigning roles for Arc onboarding. | `string` | n/a | no |
+| arcOnboardingIdentityName | The resource name for the identity used for Arc onboarding. | `string` | n/a | 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 |
+| shouldAddCurrentUserClusterAdmin | Whether to add the current user as a cluster admin. | `bool` | `true` | no |
+| shouldEnableArcAutoUpgrade | Whether to enable auto-upgrade for Azure Arc agents. | `bool` | [not(equals(parameters('common').environment, 'prod'))] | no |
+| clusterAdminOid | The Object ID that will be given cluster-admin permissions. | `string` | n/a | no |
+| clusterAdminUpn | The User Principal Name that will be given cluster-admin permissions. | `string` | n/a | no |
+| clusterNodeVirtualMachineNames | The node virtual machines names. | `array` | n/a | no |
+| clusterServerVirtualMachineName | The server virtual machines name. | `string` | n/a | no |
+| clusterServerHostMachineUsername | Username used for the host machines that will be given kube-config settings on setup. (Otherwise, resource_prefix if it exists as a user) | `string` | [parameters('common').resourcePrefix] | no |
+| clusterServerIp | The IP address for the server for the cluster. (Needed for mult-node cluster) | `string` | n/a | no |
+| serverToken | The token that will be given to the server for the cluster or used by agent nodes. | `securestring` | n/a | no |
+| shouldAssignRoles | Whether to assign roles for Arc Onboarding. | `bool` | `true` | no |
+| shouldDeployScriptToVm | Whether to deploy the scripts to the VM. | `bool` | `true` | no |
+| shouldSkipInstallingAzCli | Should skip downloading and installing Azure CLI on the server. | `bool` | `false` | no |
+| shouldSkipAzCliLogin | Should skip login process with Azure CLI on the server. | `bool` | `false` | no |
+| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | deploy-user-token | no |
+| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | n/a | yes |
+| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [resourceGroup().name] | no |
+| k3sTokenSecretName | The name for the K3s token secret in Key Vault. | `string` | k3s-server-token | no |
+| nodeScriptSecretName | The name for the node script secret in Key Vault. | `string` | cluster-node-ubuntu-k3s | no |
+| serverScriptSecretName | The name for the server script secret in Key Vault. | `string` | cluster-server-ubuntu-k3s | no |
+| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no |
## Resources
@@ -64,28 +64,28 @@ Configures K3s Kubernetes clusters on Ubuntu virtual machines and connects them
#### Parameters for ubuntuK3s
-| Name | Description | Type | Default | Required |
-|:---------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:--------|:---------|
-| common | The common component configuration. | `[_1.Common](#user-defined-types)` | n/a | yes |
-| arcResourceName | The name of the Azure Arc resource. | `string` | n/a | yes |
-| arcTenantId | The tenant ID for Azure Arc resource. | `string` | n/a | yes |
-| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
If none is provided, the script will attempt to retrieve this requiring 'Application.Read.All' or 'Directory.Read.All' permissions.
Can be retrieved using:
az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes |
-| shouldEnableArcAutoUpgrade | Whether to enable auto-upgrades for Arc agents. | `bool` | n/a | yes |
-| arcOnboardingSpClientId | The Service Principal Client ID for Arc onboarding. | `string` | n/a | no |
-| arcOnboardingSpClientSecret | The Service Principal Client Secret for Arc onboarding. | `securestring` | n/a | no |
-| clusterAdminOid | The Object ID that will be given cluster-admin permissions. | `string` | n/a | no |
-| clusterAdminUpn | The User Principal Name that will be given cluster-admin permissions. | `string` | n/a | no |
-| clusterServerHostMachineUsername | Username for the host machine with kube-config settings. | `string` | n/a | yes |
-| clusterServerIp | The IP address for the server for the cluster. (Needed for mult-node cluster) | `string` | n/a | no |
-| deployAdminOid | The Object ID that will be given deployment admin permissions. | `string` | n/a | no |
-| serverToken | The token that will be given to the server for the cluster or used by agent nodes. | `securestring` | n/a | no |
-| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | n/a | yes |
-| keyVaultName | The name of the Key Vault to save the scripts to. | `string` | n/a | yes |
-| k3sTokenSecretName | The name for the K3s token secret in Key Vault. | `string` | n/a | yes |
-| nodeScriptSecretName | The name for the node script secret in Key Vault. | `string` | n/a | yes |
-| serverScriptSecretName | The name for the server script secret in Key Vault. | `string` | n/a | yes |
-| shouldSkipAzCliLogin | Should skip login process with Azure CLI on the server. | `bool` | n/a | yes |
-| shouldSkipInstallingAzCli | Should skip downloading and installing Azure CLI on the server. | `bool` | n/a | yes |
+| Name | Description | Type | Default | Required |
+|:---------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:--------|:---------|
+| common | The common component configuration. | `[_1.Common](#user-defined-types)` | n/a | yes |
+| arcResourceName | The name of the Azure Arc resource. | `string` | n/a | yes |
+| arcTenantId | The tenant ID for Azure Arc resource. | `string` | n/a | yes |
+| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
If none is provided, the script will attempt to retrieve this requiring 'Application.Read.All' or 'Directory.Read.All' permissions.
Can be retrieved using:
az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes |
+| shouldEnableArcAutoUpgrade | Whether to enable auto-upgrades for Arc agents. | `bool` | n/a | yes |
+| arcOnboardingSpClientId | The Service Principal Client ID for Arc onboarding. | `string` | n/a | no |
+| arcOnboardingSpClientSecret | The Service Principal Client Secret for Arc onboarding. | `securestring` | n/a | no |
+| clusterAdminOid | The Object ID that will be given cluster-admin permissions. | `string` | n/a | no |
+| clusterAdminUpn | The User Principal Name that will be given cluster-admin permissions. | `string` | n/a | no |
+| clusterServerHostMachineUsername | Username for the host machine with kube-config settings. | `string` | n/a | yes |
+| clusterServerIp | The IP address for the server for the cluster. (Needed for mult-node cluster) | `string` | n/a | no |
+| deployAdminOid | The Object ID that will be given deployment admin permissions. | `string` | n/a | no |
+| serverToken | The token that will be given to the server for the cluster or used by agent nodes. | `securestring` | n/a | no |
+| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | n/a | yes |
+| keyVaultName | The name of the Key Vault to save the scripts to. | `string` | n/a | yes |
+| k3sTokenSecretName | The name for the K3s token secret in Key Vault. | `string` | n/a | yes |
+| nodeScriptSecretName | The name for the node script secret in Key Vault. | `string` | n/a | yes |
+| serverScriptSecretName | The name for the server script secret in Key Vault. | `string` | n/a | yes |
+| shouldSkipAzCliLogin | Should skip login process with Azure CLI on the server. | `bool` | n/a | yes |
+| shouldSkipInstallingAzCli | Should skip downloading and installing Azure CLI on the server. | `bool` | n/a | yes |
#### Resources for ubuntuK3s
diff --git a/src/100-edge/109-arc-extensions/README.md b/src/100-edge/109-arc-extensions/README.md
new file mode 100644
index 00000000..827f4422
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/README.md
@@ -0,0 +1,269 @@
+---
+title: Arc Extensions
+description: Component to deploy foundational Arc-enabled Kubernetes cluster extensions including cert-manager and Azure Container Storage (ACSA) required by Azure IoT Operations and other Arc-enabled services
+author: Edge AI Team
+ms.date: 2025-12-30
+ms.topic: reference
+keywords:
+ - arc extensions
+ - cert-manager
+ - azure container storage
+ - acsa
+ - edge volumes
+ - kubernetes extensions
+ - terraform
+ - bicep
+estimated_reading_time: 5
+---
+
+## Arc Extensions
+
+Component to deploy foundational Arc-enabled Kubernetes cluster extensions including cert-manager and Azure Container Storage (ACSA). These extensions provide certificate management and persistent storage capabilities required by Azure IoT Operations and other Arc-enabled services.
+
+Learn more about the required configuration by reading the [./terraform/README.md](./terraform/README.md)
+
+## Extensions Deployed
+
+This component deploys two critical Arc extensions in the correct dependency order:
+
+### cert-manager (microsoft.certmanagement)
+
+Certificate management extension that provides automated certificate lifecycle management using cert-manager and trust-manager. This extension is a foundational dependency required by:
+
+- Azure Container Storage (ACSA)
+- Azure IoT Operations Secret Store
+- Any workload requiring automated TLS certificate management
+
+**Key Features:**
+
+- Automated certificate issuance and renewal
+- Trust bundle management via trust-manager
+- Integration with various certificate issuers
+- Deployed to `cert-manager` namespace
+
+### Azure Container Storage (ACSA) (microsoft.arc.containerstorage)
+
+Provides persistent storage capabilities for Arc-enabled Kubernetes clusters with support for:
+
+- **Edge Volumes**: Local persistent storage on edge devices
+- **Cloud Ingest**: Seamless data upload from edge to Azure Blob Storage
+- **Fault-Tolerant Storage Pools**: High-availability storage configurations for multi-node clusters
+
+**Use Cases:**
+
+- Persistent data storage for IoT Operations workloads
+- Media capture and buffering for media connector
+- Local caching and data retention at the edge
+- Automated cloud upload for analytics and archival
+
+## Dependencies
+
+**Requires:**
+
+- Arc-connected Kubernetes cluster (from 100-cncf-cluster component)
+- Sufficient disk space on cluster nodes for storage volumes
+
+**Required by:**
+
+- 110-iot-ops component (requires cert-manager extension for Secret Store deployment)
+- Media connector and other services using ACSA for persistent storage
+
+**Internal Dependencies:**
+
+- Azure Container Storage extension depends on cert-manager extension (enforced via `depends_on`)
+
+## Deployment Order
+
+This component must be deployed:
+
+1. **After**: 100-cncf-cluster (Arc-connected cluster must exist)
+2. **Before**: 110-iot-ops (provides required cert-manager dependency)
+
+**Internal Extension Order:**
+
+1. cert-manager extension (foundational)
+2. Azure Container Storage extension (depends on cert-manager)
+
+## Extension Configuration
+
+Both extensions support flexible configuration options:
+
+### Common Configuration
+
+- **Version and release train**: Configure specific extension versions and stability channels
+- **Conditional deployment**: Enable/disable each extension independently via `enabled` flag
+- **Extension-specific settings**: Customize behavior per extension requirements
+
+### cert-manager Configuration
+
+Configure certificate management behavior:
+
+- **Agent operation timeout**: Timeout for extension operations (default: 20 minutes)
+- **Global telemetry**: Enable/disable telemetry collection (default: enabled)
+- **Release train**: Stable, preview, or custom channels (default: stable)
+- **Version**: Specific extension version (default: 0.7.0)
+
+### Container Storage Configuration
+
+Configure storage capabilities:
+
+- **Disk storage class**: Kubernetes storage class for persistent volumes (default: auto-detected)
+- **Fault tolerance mode**: Enable fault-tolerant storage pools for multi-node clusters (default: disabled)
+- **Disk mount point**: Host path for storage pool data (default: /mnt)
+- **Release train**: Stable, preview, or custom channels (default: stable)
+- **Version**: Specific extension version (default: 2.6.0)
+
+**Note**: Fault tolerance requires multi-node clusters and consumes additional disk space for replication.
+
+## Terraform
+
+Refer to [Terraform Components - Getting Started](../README.md#terraform-components---getting-started) for deployment instructions.
+
+Learn more about the required configuration by reading the [./terraform/README.md](./terraform/README.md)
+
+### Example Configuration
+
+Add the following to your `terraform.tfvars` file:
+
+```hcl
+# Enable both extensions with default settings
+arc_extensions = {
+ cert_manager_extension = {
+ enabled = true
+ version = "0.7.0"
+ train = "stable"
+ agent_operation_timeout_in_minutes = 20
+ global_telemetry_enabled = true
+ }
+
+ container_storage_extension = {
+ enabled = true
+ version = "2.6.0"
+ train = "stable"
+ disk_storage_class = "" # Auto-detect
+ fault_tolerance_enabled = false
+ disk_mount_point = "/mnt"
+ }
+}
+```
+
+### Disable Specific Extensions
+
+To deploy only cert-manager without ACSA:
+
+```hcl
+arc_extensions = {
+ cert_manager_extension = {
+ enabled = true
+ version = "0.7.0"
+ train = "stable"
+ agent_operation_timeout_in_minutes = 20
+ global_telemetry_enabled = true
+ }
+
+ container_storage_extension = {
+ enabled = false
+ version = "2.6.0"
+ train = "stable"
+ disk_storage_class = ""
+ fault_tolerance_enabled = false
+ disk_mount_point = "/mnt"
+ }
+}
+```
+
+## Bicep
+
+Learn more about the required configuration by reading the [./bicep/README.md](./bicep/README.md)
+
+## Troubleshooting
+
+### Extension Installation Issues
+
+Check extension status using Azure CLI:
+
+```sh
+# List all extensions on the cluster
+az k8s-extension list \
+ --cluster-name \
+ --resource-group \
+ --cluster-type connectedClusters
+
+# Check specific extension status
+az k8s-extension show \
+ --name certmanager \
+ --cluster-name \
+ --resource-group \
+ --cluster-type connectedClusters
+```
+
+### cert-manager Issues
+
+Verify cert-manager pods are running:
+
+```sh
+kubectl get pods -n cert-manager
+```
+
+Check cert-manager logs:
+
+```sh
+kubectl logs -n cert-manager -l app=cert-manager
+kubectl logs -n cert-manager -l app=webhook
+kubectl logs -n cert-manager -l app=cainjector
+```
+
+### Azure Container Storage Issues
+
+Verify ACSA pods are running:
+
+```sh
+kubectl get pods -n azure-arc-containerstorage
+```
+
+Check storage pool status:
+
+```sh
+kubectl get storagepools -A
+```
+
+View ACSA logs:
+
+```sh
+kubectl logs -n azure-arc-containerstorage -l app=azure-arc-containerstorage
+```
+
+### Common Issues
+
+**Extension installation timeout**:
+
+- Increase `agent_operation_timeout_in_minutes` for cert-manager
+- Verify cluster has internet connectivity to download extension components
+
+**ACSA fails to create storage pool**:
+
+- Verify sufficient disk space at configured `disk_mount_point`
+- Check node has required kernel modules loaded
+- For fault tolerance, verify cluster has multiple nodes
+
+**cert-manager webhook errors**:
+
+- Verify cluster DNS is functioning correctly
+- Check webhook service and endpoints are created
+
+## References
+
+- [Azure Arc Extensions Overview](https://learn.microsoft.com/azure/azure-arc/kubernetes/extensions)
+- [cert-manager Documentation](https://cert-manager.io/docs/)
+- [Azure Container Storage for Arc](https://learn.microsoft.com/azure/azure-arc/container-storage/)
+- [Install Edge Volumes](https://learn.microsoft.com/azure/azure-arc/container-storage/howto-install-edge-volumes?tabs=single)
+- [Configure Cloud Ingest](https://learn.microsoft.com/azure/azure-arc/container-storage/howto-configure-cloud-ingest-subvolumes?tabs=portal)
+- [Multi-Node Storage Configuration](https://learn.microsoft.com/azure/iot-operations/deploy-iot-ops/howto-prepare-cluster?tabs=ubuntu#configure-multi-node-clusters-for-azure-container-storage-enabled-by-azure-arc)
+- [Media Connector with ACSA](https://learn.microsoft.com/azure/iot-operations/discover-manage-assets/howto-use-media-connector?tabs=portal#deploy-the-media-connector)
+
+---
+
+
+*🤖 Crafted with precision by ✨Copilot following brilliant human instruction,
+then carefully refined by our team of discerning human reviewers.*
+
diff --git a/src/100-edge/109-arc-extensions/bicep/README.md b/src/100-edge/109-arc-extensions/bicep/README.md
new file mode 100644
index 00000000..4a450a24
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/bicep/README.md
@@ -0,0 +1,64 @@
+
+
+
+# Arc Extensions
+
+Deploys foundational Arc-enabled Kubernetes cluster extensions including cert-manager and Azure Container Storage (ACSA).
+
+## Parameters
+
+| Name | Description | Type | Default | Required |
+|:------------------------|:----------------------------------------------------------------------|:------------------------------------------------------|:----------------------------------------------------|:---------|
+| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes |
+| certManagerConfig | The settings for the cert-manager Extension. | `[_1.CertManagerExtension](#user-defined-types)` | [variables('_1.certManagerExtensionDefaults')] | no |
+| containerStorageConfig | The settings for the Azure Container Storage for Azure Arc Extension. | `[_1.ContainerStorageExtension](#user-defined-types)` | [variables('_1.containerStorageExtensionDefaults')] | no |
+
+## Resources
+
+| Name | Type | API Version |
+|:-----------------|:-----------------------------------------------|:------------|
+| aioCertManager | `Microsoft.KubernetesConfiguration/extensions` | 2024-11-01 |
+| containerStorage | `Microsoft.KubernetesConfiguration/extensions` | 2024-11-01 |
+
+## User Defined Types
+
+### `_1.CertManagerExtension`
+
+The settings for the cert-manager Extension.
+
+| Property | Type | Description |
+|:---------|:------------------------------------|:----------------------------------------------|
+| enabled | `bool` | Whether to deploy the cert-manager extension. |
+| release | `[_1.Release](#user-defined-types)` | The common settings for the extension. |
+| settings | `object` | |
+
+### `_1.ContainerStorageExtension`
+
+The settings for the Azure Container Storage for Azure Arc Extension.
+
+| Property | Type | Description |
+|:---------|:------------------------------------|:---------------------------------------------------|
+| enabled | `bool` | Whether to deploy the container storage extension. |
+| release | `[_1.Release](#user-defined-types)` | The common settings for the extension. |
+| settings | `object` | |
+
+### `_1.Release`
+
+The common settings for Azure Arc Extensions.
+
+| Property | Type | Description |
+|:------------------------|:---------|:-----------------------------------------------------------------------------|
+| version | `string` | The version of the extension. |
+| train | `string` | The release train that has the version to deploy (ex., "preview", "stable"). |
+| autoUpgradeMinorVersion | `bool` | Whether to automatically upgrade minor versions of the extension. |
+
+## Outputs
+
+| Name | Type | Description |
+|:------------------------------|:---------|:----------------------------------------------------------|
+| certManagerExtensionId | `string` | The resource ID of the cert-manager extension. |
+| certManagerExtensionName | `string` | The name of the cert-manager extension. |
+| containerStorageExtensionId | `string` | The resource ID of the Azure Container Storage extension. |
+| containerStorageExtensionName | `string` | The name of the Azure Container Storage extension. |
+
+
\ No newline at end of file
diff --git a/src/100-edge/109-arc-extensions/bicep/main.bicep b/src/100-edge/109-arc-extensions/bicep/main.bicep
new file mode 100644
index 00000000..ec751a77
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/bicep/main.bicep
@@ -0,0 +1,112 @@
+metadata name = 'Arc Extensions'
+metadata description = 'Deploys foundational Arc-enabled Kubernetes cluster extensions including cert-manager and Azure Container Storage (ACSA).'
+
+import * as types from './types.bicep'
+
+/*
+ Common Parameters
+*/
+
+@description('The resource name for the Arc connected cluster.')
+param arcConnectedClusterName string
+
+/*
+ Extension Parameters
+*/
+
+@description('The settings for the cert-manager Extension.')
+param certManagerConfig types.CertManagerExtension = types.certManagerExtensionDefaults
+
+@description('The settings for the Azure Container Storage for Azure Arc Extension.')
+param containerStorageConfig types.ContainerStorageExtension = types.containerStorageExtensionDefaults
+
+/*
+ Existing Resources
+*/
+
+resource arcConnectedCluster 'Microsoft.Kubernetes/connectedClusters@2024-12-01-preview' existing = {
+ name: arcConnectedClusterName
+}
+
+/*
+ cert-manager Extension
+*/
+
+resource aioCertManager 'Microsoft.KubernetesConfiguration/extensions@2024-11-01' = if (certManagerConfig.enabled) {
+ name: 'arc-cert-manager'
+ scope: arcConnectedCluster
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ extensionType: 'microsoft.certmanagement'
+ version: certManagerConfig.release.version
+ releaseTrain: certManagerConfig.release.train
+ autoUpgradeMinorVersion: certManagerConfig.release.?autoUpgradeMinorVersion ?? false
+ scope: {
+ cluster: {
+ releaseNamespace: 'cert-manager'
+ }
+ }
+ configurationSettings: {
+ AgentOperationTimeoutInMinutes: certManagerConfig.settings.?agentOperationTimeoutInMinutes
+ 'global.telemetry.enabled': string(certManagerConfig.settings.?globalTelemetryEnabled ?? true)
+ }
+ }
+}
+
+/*
+ Azure Container Storage Extension
+*/
+
+var defaultStorageClass = containerStorageConfig.settings.?faultToleranceEnabled
+ ? 'acstor-arccontainerstorage-storage-pool'
+ : 'default,local-path'
+var kubernetesStorageClass = containerStorageConfig.settings.?diskStorageClass ?? defaultStorageClass
+var diskMountPoint = containerStorageConfig.settings.?diskMountPoint ?? '/mnt'
+
+var containerStorageSettings = containerStorageConfig.settings.?faultToleranceEnabled
+ ? {
+ 'edgeStorageConfiguration.create': 'true'
+ 'feature.diskStorageClass': kubernetesStorageClass
+ 'acstorConfiguration.create': 'true'
+ 'acstorConfiguration.properties.diskMountPoint': diskMountPoint
+ }
+ : {
+ 'edgeStorageConfiguration.create': 'true'
+ 'feature.diskStorageClass': kubernetesStorageClass
+ }
+
+resource containerStorage 'Microsoft.KubernetesConfiguration/extensions@2024-11-01' = if (containerStorageConfig.enabled) {
+ name: 'azure-arc-containerstorage'
+ scope: arcConnectedCluster
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ extensionType: 'microsoft.arc.containerstorage'
+ version: containerStorageConfig.release.version
+ releaseTrain: containerStorageConfig.release.train
+ autoUpgradeMinorVersion: false
+ configurationSettings: containerStorageSettings
+ }
+ dependsOn: [
+ aioCertManager
+ ]
+}
+
+/*
+ Outputs
+*/
+
+@description('The resource ID of the cert-manager extension.')
+output certManagerExtensionId string = certManagerConfig.enabled ? aioCertManager.id : ''
+
+@description('The name of the cert-manager extension.')
+output certManagerExtensionName string = certManagerConfig.enabled ? aioCertManager.name : ''
+
+@description('The resource ID of the Azure Container Storage extension.')
+output containerStorageExtensionId string = containerStorageConfig.enabled ? containerStorage.id : ''
+
+@description('The name of the Azure Container Storage extension.')
+output containerStorageExtensionName string = containerStorageConfig.enabled ? containerStorage.name : ''
diff --git a/src/100-edge/109-arc-extensions/bicep/types.bicep b/src/100-edge/109-arc-extensions/bicep/types.bicep
new file mode 100644
index 00000000..45ed4db3
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/bicep/types.bicep
@@ -0,0 +1,83 @@
+/*
+ * IMPORTANT: The variable names in this file ('certManagerExtensionDefaults',
+ * 'containerStorageExtensionDefaults') are explicitly referenced
+ * by the aio-version-checker.py script. If you rename these variables or change their structure,
+ * you must also update the script and the aio-version-checker-template.yml pipeline.
+ */
+
+@export()
+@description('The common settings for Azure Arc Extensions.')
+type Release = {
+ @description('The version of the extension.')
+ version: string
+
+ @description('The release train that has the version to deploy (ex., "preview", "stable").')
+ train: string
+
+ @description('Whether to automatically upgrade minor versions of the extension.')
+ autoUpgradeMinorVersion: bool?
+}
+
+@export()
+@description('The settings for the cert-manager Extension.')
+type CertManagerExtension = {
+ @description('Whether to deploy the cert-manager extension.')
+ enabled: bool
+
+ @description('The common settings for the extension.')
+ release: Release
+
+ settings: {
+ @description('Agent operation timeout in minutes.')
+ agentOperationTimeoutInMinutes: string
+ @description('Enable or disable global telemetry.')
+ globalTelemetryEnabled: bool?
+ }
+}
+
+@export()
+var certManagerExtensionDefaults = {
+ enabled: true
+ release: {
+ version: '0.7.0'
+ train: 'stable'
+ autoUpgradeMinorVersion: false
+ }
+ settings: {
+ agentOperationTimeoutInMinutes: '20'
+ globalTelemetryEnabled: true
+ }
+}
+
+@export()
+@description('The settings for the Azure Container Storage for Azure Arc Extension.')
+type ContainerStorageExtension = {
+ @description('Whether to deploy the container storage extension.')
+ enabled: bool
+
+ @description('The common settings for the extension.')
+ release: Release
+
+ settings: {
+ @description('Whether or not to enable fault tolerant storage in the cluster.')
+ faultToleranceEnabled: bool
+
+ @description('The storage class for the cluster. (Otherwise, "acstor-arccontainerstorage-storage-pool" for fault tolerant storage else "default,local-path")')
+ diskStorageClass: string?
+
+ @description('The disk mount point for the cluster when using fault tolerant storage. (Otherwise: "/mnt" when using fault tolerant storage)')
+ diskMountPoint: string?
+ }
+}
+
+@export()
+var containerStorageExtensionDefaults = {
+ enabled: true
+ release: {
+ version: '2.6.0'
+ train: 'stable'
+ }
+ settings: {
+ faultToleranceEnabled: false
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/bicep/types.core.bicep b/src/100-edge/109-arc-extensions/bicep/types.core.bicep
new file mode 100644
index 00000000..7f61cc1d
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/bicep/types.core.bicep
@@ -0,0 +1,15 @@
+@export()
+@description('The common component configuration.')
+type Common = {
+ @description('The environment for all resources. Example values: dev, test, prod.')
+ environment: string
+
+ @description('The prefix for all resources.')
+ resourcePrefix: string
+
+ @description('The location/region for all resources.')
+ location: string
+
+ @description('The instance identifier for naming resources. Example values: 001, 002, etc.')
+ instance: string
+}
diff --git a/src/100-edge/109-arc-extensions/ci/bicep/main.bicep b/src/100-edge/109-arc-extensions/ci/bicep/main.bicep
new file mode 100644
index 00000000..552b1662
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/ci/bicep/main.bicep
@@ -0,0 +1,40 @@
+metadata name = 'Arc Extensions CI'
+metadata description = 'CI deployment for Arc Extensions component.'
+
+import * as core from '../../bicep/types.core.bicep'
+
+/*
+ Common Parameters
+*/
+
+@description('The common component configuration.')
+param common core.Common
+
+/*
+ Existing Resources
+*/
+
+var resourceGroupName = 'rg-${common.resourcePrefix}-${common.environment}-${common.instance}'
+var arcConnectedClusterName = 'arck-${common.resourcePrefix}-${common.environment}-${common.instance}'
+
+resource resourceGroup 'Microsoft.Resources/resourceGroups@2024-03-01' existing = {
+ name: resourceGroupName
+ scope: subscription()
+}
+
+resource arcConnectedCluster 'Microsoft.Kubernetes/connectedClusters@2024-12-01-preview' existing = {
+ name: arcConnectedClusterName
+ scope: resourceGroup
+}
+
+/*
+ Module Deployment
+*/
+
+module ci '../../bicep/main.bicep' = {
+ name: 'arc-extensions-ci-deployment'
+ scope: resourceGroup
+ params: {
+ arcConnectedClusterName: arcConnectedCluster.name
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/ci/terraform/main.tf b/src/100-edge/109-arc-extensions/ci/terraform/main.tf
new file mode 100644
index 00000000..8fe29d85
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/ci/terraform/main.tf
@@ -0,0 +1,24 @@
+resource "terraform_data" "defer" {
+ input = {
+ resource_group_name = "rg-${var.resource_prefix}-${var.environment}-${var.instance}"
+ arc_connected_cluster_name = "arck-${var.resource_prefix}-${var.environment}-${var.instance}"
+ }
+}
+
+data "azurerm_resource_group" "arc_extensions" {
+ name = terraform_data.defer.input.resource_group_name
+}
+
+data "azapi_resource" "arc_connected_cluster" {
+ type = "Microsoft.Kubernetes/connectedClusters@2024-01-01"
+ parent_id = data.azurerm_resource_group.arc_extensions.id
+ name = terraform_data.defer.input.arc_connected_cluster_name
+
+ response_export_values = ["name", "id", "location"]
+}
+
+module "ci" {
+ source = "../../terraform"
+
+ arc_connected_cluster = data.azapi_resource.arc_connected_cluster.output
+}
diff --git a/src/100-edge/109-arc-extensions/ci/terraform/variables.tf b/src/100-edge/109-arc-extensions/ci/terraform/variables.tf
new file mode 100644
index 00000000..0599151c
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/ci/terraform/variables.tf
@@ -0,0 +1,19 @@
+variable "environment" {
+ type = string
+ description = "Environment for all resources in this module: dev, test, or prod"
+}
+
+variable "resource_prefix" {
+ type = string
+ validation {
+ condition = length(var.resource_prefix) > 0 && can(regex("^[a-zA-Z](?:-?[a-zA-Z0-9])*$", var.resource_prefix))
+ error_message = "Resource prefix must not be empty, must only contain alphanumeric characters and dashes. Must start with an alphabetic character."
+ }
+ description = "Prefix for all resources in this module"
+}
+
+variable "instance" {
+ type = string
+ description = "Instance identifier for naming resources: 001, 002, etc"
+ default = "001"
+}
diff --git a/src/100-edge/109-arc-extensions/ci/terraform/versions.tf b/src/100-edge/109-arc-extensions/ci/terraform/versions.tf
new file mode 100644
index 00000000..3f132b50
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/ci/terraform/versions.tf
@@ -0,0 +1,18 @@
+terraform {
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = ">= 4.8.0"
+ }
+ azapi = {
+ source = "Azure/azapi"
+ version = ">= 2.3.0"
+ }
+ }
+ required_version = ">= 1.9.8, < 2.0"
+}
+
+provider "azurerm" {
+ storage_use_azuread = true
+ features {}
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/README.md b/src/100-edge/109-arc-extensions/terraform/README.md
new file mode 100644
index 00000000..8349250d
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/README.md
@@ -0,0 +1,38 @@
+
+# Arc Extensions
+
+Deploys foundational Arc-enabled Kubernetes cluster extensions including
+cert-manager and Azure Container Storage (ACSA).
+
+## Requirements
+
+| Name | Version |
+|-----------|-----------------|
+| terraform | >= 1.9.8, < 2.0 |
+| azurerm | >= 4.8.0 |
+
+## Modules
+
+| Name | Source | Version |
+|-------------------------------|-----------------------------|---------|
+| cert\_manager\_extension | ./modules/cert-manager | n/a |
+| container\_storage\_extension | ./modules/container-storage | n/a |
+
+## Inputs
+
+| 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.7.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
+
+| Name | Description |
+|-------------------------------------|------------------------------------------------------------------------------------------------------|
+| cert\_manager\_extension | Self-contained cert\_manager object (id, name, enabled, version, train) or null if not deployed |
+| cert\_manager\_extension\_id | The resource ID of the cert-manager extension. |
+| cert\_manager\_extension\_name | The name of the cert-manager extension. |
+| container\_storage\_extension | Self-contained container\_storage object (id, name, enabled, version, train) or null if not deployed |
+| container\_storage\_extension\_id | The resource ID of the Azure Container Storage extension. |
+| container\_storage\_extension\_name | The name of the Azure Container Storage extension. |
+
diff --git a/src/100-edge/109-arc-extensions/terraform/main.tf b/src/100-edge/109-arc-extensions/terraform/main.tf
new file mode 100644
index 00000000..79e5d4bf
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/main.tf
@@ -0,0 +1,26 @@
+/**
+ * # Arc Extensions
+ *
+ * Deploys foundational Arc-enabled Kubernetes cluster extensions including
+ * cert-manager and Azure Container Storage (ACSA).
+ */
+
+module "cert_manager_extension" {
+ count = var.arc_extensions.cert_manager_extension.enabled ? 1 : 0
+
+ source = "./modules/cert-manager"
+
+ arc_connected_cluster_id = var.arc_connected_cluster.id
+ cert_manager_extension = var.arc_extensions.cert_manager_extension
+}
+
+module "container_storage_extension" {
+ count = var.arc_extensions.container_storage_extension.enabled ? 1 : 0
+
+ source = "./modules/container-storage"
+
+ depends_on = [module.cert_manager_extension]
+
+ arc_connected_cluster_id = var.arc_connected_cluster.id
+ container_storage_extension = var.arc_extensions.container_storage_extension
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/README.md b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/README.md
new file mode 100644
index 00000000..9a3d9fa0
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/README.md
@@ -0,0 +1,37 @@
+
+# cert-manager Extension Module
+
+Deploys the cert-manager extension for Arc-enabled Kubernetes clusters.
+
+## Requirements
+
+| Name | Version |
+|-----------|-----------------|
+| terraform | >= 1.9.8, < 2.0 |
+
+## Providers
+
+| Name | Version |
+|---------|---------|
+| azurerm | n/a |
+
+## Resources
+
+| Name | Type |
+|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
+| [azurerm_arc_kubernetes_cluster_extension.cert_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/arc_kubernetes_cluster_extension) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|-----------------------------|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|:--------:|
+| arc\_connected\_cluster\_id | The resource ID of the Arc-connected Kubernetes cluster | `string` | n/a | yes |
+| cert\_manager\_extension | cert-manager extension configuration object | ```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) })``` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|---------------|-----------------------------------------------------|
+| cert\_manager | Self-contained cert-manager object |
+| extension | The cert-manager extension id and name as an object |
+
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/main.tf b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/main.tf
new file mode 100644
index 00000000..7eafa300
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/main.tf
@@ -0,0 +1,21 @@
+/**
+ * # cert-manager Extension Module
+ *
+ * Deploys the cert-manager extension for Arc-enabled Kubernetes clusters.
+ */
+
+resource "azurerm_arc_kubernetes_cluster_extension" "cert_manager" {
+ name = "arc-cert-manager"
+ cluster_id = var.arc_connected_cluster_id
+ extension_type = "microsoft.certmanagement"
+ identity {
+ type = "SystemAssigned"
+ }
+ version = var.cert_manager_extension.version
+ release_train = var.cert_manager_extension.train
+ release_namespace = "cert-manager"
+ configuration_settings = {
+ "AgentOperationTimeoutInMinutes" = tostring(var.cert_manager_extension.agent_operation_timeout_in_minutes)
+ "global.telemetry.enabled" = var.cert_manager_extension.global_telemetry_enabled
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/outputs.tf b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/outputs.tf
new file mode 100644
index 00000000..89a80c8d
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/outputs.tf
@@ -0,0 +1,18 @@
+output "cert_manager" {
+ description = "Self-contained cert-manager object"
+ value = {
+ enabled = var.cert_manager_extension.enabled
+ id = azurerm_arc_kubernetes_cluster_extension.cert_manager.id
+ name = azurerm_arc_kubernetes_cluster_extension.cert_manager.name
+ version = var.cert_manager_extension.version
+ train = var.cert_manager_extension.train
+ }
+}
+
+output "extension" {
+ description = "The cert-manager extension id and name as an object"
+ value = {
+ id = azurerm_arc_kubernetes_cluster_extension.cert_manager.id
+ name = azurerm_arc_kubernetes_cluster_extension.cert_manager.name
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/variables.tf b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/variables.tf
new file mode 100644
index 00000000..868daa77
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/variables.tf
@@ -0,0 +1,15 @@
+variable "arc_connected_cluster_id" {
+ type = string
+ description = "The resource ID of the Arc-connected Kubernetes cluster"
+}
+variable "cert_manager_extension" {
+ type = 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)
+ })
+ description = "cert-manager extension configuration object"
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/versions.tf b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/versions.tf
new file mode 100644
index 00000000..cb47f2e9
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/cert-manager/versions.tf
@@ -0,0 +1,8 @@
+terraform {
+ required_version = ">= 1.9.8, < 2.0"
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ }
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/container-storage/README.md b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/README.md
new file mode 100644
index 00000000..030b415a
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/README.md
@@ -0,0 +1,37 @@
+
+# Azure Container Storage Extension Module
+
+Deploys the Azure Container Storage (ACSA) extension for Arc-enabled Kubernetes clusters.
+
+## Requirements
+
+| Name | Version |
+|-----------|-----------------|
+| terraform | >= 1.9.8, < 2.0 |
+
+## Providers
+
+| Name | Version |
+|---------|---------|
+| azurerm | n/a |
+
+## Resources
+
+| Name | Type |
+|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
+| [azurerm_arc_kubernetes_cluster_extension.container_storage](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/arc_kubernetes_cluster_extension) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|-------------------------------|---------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|:--------:|
+| arc\_connected\_cluster\_id | The resource ID of the Arc-connected Kubernetes cluster | `string` | n/a | yes |
+| container\_storage\_extension | container-storage extension configuration object | ```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) })``` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|--------------------|----------------------------------------------------------|
+| container\_storage | Self-contained container\_storage object |
+| extension | The container storage extension id and name as an object |
+
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/container-storage/main.tf b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/main.tf
new file mode 100644
index 00000000..3ed887c3
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/main.tf
@@ -0,0 +1,32 @@
+/**
+ * # Azure Container Storage Extension Module
+ *
+ * Deploys the Azure Container Storage (ACSA) extension for Arc-enabled Kubernetes clusters.
+ */
+
+locals {
+ default_storage_class = var.container_storage_extension.fault_tolerance_enabled ? "acstor-arccontainerstorage-storage-pool" : "default,local-path"
+ kubernetes_storage_class = var.container_storage_extension.disk_storage_class != "" ? var.container_storage_extension.disk_storage_class : local.default_storage_class
+
+ container_storage_settings = var.container_storage_extension.fault_tolerance_enabled ? {
+ "edgeStorageConfiguration.create" = "true"
+ "feature.diskStorageClass" = local.kubernetes_storage_class
+ "acstorConfiguration.create" = "true"
+ "acstorConfiguration.properties.diskMountPoint" = var.container_storage_extension.disk_mount_point
+ } : {
+ "edgeStorageConfiguration.create" = "true"
+ "feature.diskStorageClass" = local.kubernetes_storage_class
+ }
+}
+
+resource "azurerm_arc_kubernetes_cluster_extension" "container_storage" {
+ name = "azure-arc-containerstorage"
+ cluster_id = var.arc_connected_cluster_id
+ extension_type = "microsoft.arc.containerstorage"
+ identity {
+ type = "SystemAssigned"
+ }
+ version = var.container_storage_extension.version
+ release_train = var.container_storage_extension.train
+ configuration_settings = local.container_storage_settings
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/container-storage/outputs.tf b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/outputs.tf
new file mode 100644
index 00000000..7f7c3cee
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/outputs.tf
@@ -0,0 +1,18 @@
+output "container_storage" {
+ description = "Self-contained container_storage object"
+ value = {
+ enabled = var.container_storage_extension.enabled
+ id = azurerm_arc_kubernetes_cluster_extension.container_storage.id
+ name = azurerm_arc_kubernetes_cluster_extension.container_storage.name
+ version = var.container_storage_extension.version
+ train = var.container_storage_extension.train
+ }
+}
+
+output "extension" {
+ description = "The container storage extension id and name as an object"
+ value = {
+ id = azurerm_arc_kubernetes_cluster_extension.container_storage.id
+ name = azurerm_arc_kubernetes_cluster_extension.container_storage.name
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/container-storage/variables.tf b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/variables.tf
new file mode 100644
index 00000000..a18680ab
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/variables.tf
@@ -0,0 +1,16 @@
+variable "arc_connected_cluster_id" {
+ type = string
+ description = "The resource ID of the Arc-connected Kubernetes cluster"
+}
+variable "container_storage_extension" {
+ type = 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)
+ })
+ description = "container-storage extension configuration object"
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/modules/container-storage/versions.tf b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/versions.tf
new file mode 100644
index 00000000..cb47f2e9
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/modules/container-storage/versions.tf
@@ -0,0 +1,8 @@
+terraform {
+ required_version = ">= 1.9.8, < 2.0"
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ }
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/outputs.tf b/src/100-edge/109-arc-extensions/terraform/outputs.tf
new file mode 100644
index 00000000..10df3a72
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/outputs.tf
@@ -0,0 +1,29 @@
+output "cert_manager_extension_id" {
+ description = "The resource ID of the cert-manager extension."
+ value = try(module.cert_manager_extension[0].extension.id, null)
+}
+
+output "cert_manager_extension_name" {
+ description = "The name of the cert-manager extension."
+ value = try(module.cert_manager_extension[0].extension.name, null)
+}
+
+output "container_storage_extension_id" {
+ description = "The resource ID of the Azure Container Storage extension."
+ value = try(module.container_storage_extension[0].extension.id, null)
+}
+
+output "container_storage_extension_name" {
+ description = "The name of the Azure Container Storage extension."
+ value = try(module.container_storage_extension[0].extension.name, null)
+}
+
+output "cert_manager_extension" {
+ description = "Self-contained cert_manager object (id, name, enabled, version, train) or null if not deployed"
+ value = try(module.cert_manager_extension[0].cert_manager, null)
+}
+
+output "container_storage_extension" {
+ description = "Self-contained container_storage object (id, name, enabled, version, train) or null if not deployed"
+ value = try(module.container_storage_extension[0].container_storage, null)
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/variables.deps.tf b/src/100-edge/109-arc-extensions/terraform/variables.deps.tf
new file mode 100644
index 00000000..9fecb437
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/variables.deps.tf
@@ -0,0 +1,8 @@
+variable "arc_connected_cluster" {
+ type = object({
+ id = string
+ name = string
+ location = string
+ })
+ description = "Arc-connected Kubernetes cluster object containing id, name, and location"
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/variables.tf b/src/100-edge/109-arc-extensions/terraform/variables.tf
new file mode 100644
index 00000000..86ff9e2d
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/variables.tf
@@ -0,0 +1,41 @@
+variable "arc_extensions" {
+ type = 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)
+ }))
+ })
+ description = "Combined configuration object for Arc extensions (cert-manager and container storage)"
+ default = {
+ cert_manager_extension = {
+ enabled = true
+ version = "0.7.0"
+ train = "stable"
+ auto_upgrade_minor_version = false
+ agent_operation_timeout_in_minutes = 20
+ global_telemetry_enabled = true
+ }
+ container_storage_extension = {
+ enabled = true
+ version = "2.6.0"
+ train = "stable"
+ auto_upgrade_minor_version = false
+ disk_storage_class = ""
+ fault_tolerance_enabled = false
+ disk_mount_point = "/mnt"
+ }
+ }
+}
diff --git a/src/100-edge/109-arc-extensions/terraform/versions.tf b/src/100-edge/109-arc-extensions/terraform/versions.tf
new file mode 100644
index 00000000..cdcce40a
--- /dev/null
+++ b/src/100-edge/109-arc-extensions/terraform/versions.tf
@@ -0,0 +1,9 @@
+terraform {
+ required_version = ">= 1.9.8, < 2.0"
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = ">= 4.8.0"
+ }
+ }
+}
diff --git a/src/100-edge/110-iot-ops/bicep/README.md b/src/100-edge/110-iot-ops/bicep/README.md
index 0da7c07d..0862c5be 100644
--- a/src/100-edge/110-iot-ops/bicep/README.md
+++ b/src/100-edge/110-iot-ops/bicep/README.md
@@ -7,51 +7,49 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure
## Parameters
-| Name | Description | Type | Default | Required |
-|:------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------|:---------|
-| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes |
-| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes |
-| containerStorageConfig | The settings for the Azure Container Store for Azure Arc Extension. | `[_1.ContainerStorageExtension](#user-defined-types)` | [variables('_1.containerStorageExtensionDefaults')] | no |
-| aioCertManagerConfig | The settings for the Azure IoT Operations Platform Extension. | `[_1.AioCertManagerExtension](#user-defined-types)` | [variables('_1.aioCertManagerExtensionDefaults')] | no |
-| secretStoreConfig | The settings for the Secret Store Extension. | `[_1.SecretStoreExtension](#user-defined-types)` | [variables('_1.secretStoreExtensionDefaults')] | no |
-| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no |
-| aioIdentityName | The name of the User Assigned Managed Identity for Azure IoT Operations. | `string` | n/a | yes |
-| aioExtensionConfig | The settings for the Azure IoT Operations Extension. | `[_1.AioExtension](#user-defined-types)` | [variables('_1.aioExtensionDefaults')] | no |
-| aioFeatures | AIO Instance features. | `[_1.AioFeatures](#user-defined-types)` | n/a | no |
-| aioInstanceName | The name for the Azure IoT Operations Instance resource. | `string` | [format('{0}-ops-instance', parameters('arcConnectedClusterName'))] | no |
-| aioDataFlowInstanceConfig | The settings for Azure IoT Operations Data Flow Instances. | `[_1.AioDataFlowInstance](#user-defined-types)` | [variables('_1.aioDataFlowInstanceDefaults')] | no |
-| aioMqBrokerConfig | The settings for the Azure IoT Operations MQ Broker. | `[_1.AioMqBroker](#user-defined-types)` | [variables('_1.aioMqBrokerDefaults')] | no |
-| brokerListenerAnonymousConfig | Configuration for the insecure anonymous AIO MQ Broker Listener. | `[_1.AioMqBrokerAnonymous](#user-defined-types)` | [variables('_1.aioMqBrokerAnonymousDefaults')] | no |
-| configurationSettingsOverride | Optional configuration settings to override default IoT Operations extension configuration. Use the same key names as the az iot ops --ops-config parameter. | `object` | {} | no |
-| schemaRegistryName | The resource name for the ADR Schema Registry for Azure IoT Operations. | `string` | n/a | yes |
-| adrNamespaceName | The resource name for the ADR Namespace for Azure IoT Operations. | `string` | n/a | no |
-| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no |
-| shouldDeployResourceSyncRules | Whether or not to deploy the Custom Locations Resource Sync Rules for the Azure IoT Operations resources. | `bool` | `true` | no |
-| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `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 for Azure IoT Operations. | `bool` | `true` | no |
-| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no |
-| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no |
-| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no |
-| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no |
-| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no |
-| akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no |
-| customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no |
-| additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no |
-| trustIssuerSettings | The trust issuer settings for Customer Managed Azure IoT Operations Settings. | `[_1.TrustIssuerConfig](#user-defined-types)` | {'trustSource': 'SelfSigned'} | no |
-| sseKeyVaultName | The name of the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | n/a | yes |
-| sseIdentityName | The name of the User Assigned Managed Identity for Secret Sync. | `string` | n/a | yes |
-| sseKeyVaultResourceGroupName | The name of the Resource Group for the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | [resourceGroup().name] | no |
-| shouldAssignSseKeyVaultRoles | Whether to assign roles for Key Vault to the provided Secret Sync Identity. | `bool` | `true` | no |
-| shouldAssignDeployIdentityRoles | Whether to assign roles to the deploy identity. | `bool` | [not(empty(parameters('deployIdentityName')))] | no |
-| deployIdentityName | The resource name for a managed identity that will be given deployment admin permissions. | `string` | n/a | no |
-| shouldDeployAioDeploymentScripts | Whether to deploy DeploymentScripts for Azure IoT Operations. | `bool` | `false` | no |
-| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | [parameters('sseKeyVaultName')] | no |
-| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [parameters('sseKeyVaultResourceGroupName')] | no |
-| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | deploy-user-token | no |
-| deploymentScriptsSecretNamePrefix | The prefix used with constructing the secret name that will have the deployment script. | `string` | [format('{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no |
-| shouldAddDeployScriptsToKeyVault | Whether to add the deploy scripts for DeploymentScripts to Key Vault as secrets. (Required for DeploymentScripts) | `bool` | `false` | no |
-| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no |
+| Name | Description | Type | Default | Required |
+|:------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------|:---------|
+| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes |
+| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes |
+| secretStoreConfig | The settings for the Secret Store Extension. | `[_1.SecretStoreExtension](#user-defined-types)` | [variables('_1.secretStoreExtensionDefaults')] | no |
+| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no |
+| aioIdentityName | The name of the User Assigned Managed Identity for Azure IoT Operations. | `string` | n/a | yes |
+| aioExtensionConfig | The settings for the Azure IoT Operations Extension. | `[_1.AioExtension](#user-defined-types)` | [variables('_1.aioExtensionDefaults')] | no |
+| aioFeatures | AIO Instance features. | `[_1.AioFeatures](#user-defined-types)` | n/a | no |
+| aioInstanceName | The name for the Azure IoT Operations Instance resource. | `string` | [format('{0}-ops-instance', parameters('arcConnectedClusterName'))] | no |
+| aioDataFlowInstanceConfig | The settings for Azure IoT Operations Data Flow Instances. | `[_1.AioDataFlowInstance](#user-defined-types)` | [variables('_1.aioDataFlowInstanceDefaults')] | no |
+| aioMqBrokerConfig | The settings for the Azure IoT Operations MQ Broker. | `[_1.AioMqBroker](#user-defined-types)` | [variables('_1.aioMqBrokerDefaults')] | no |
+| brokerListenerAnonymousConfig | Configuration for the insecure anonymous AIO MQ Broker Listener. | `[_1.AioMqBrokerAnonymous](#user-defined-types)` | [variables('_1.aioMqBrokerAnonymousDefaults')] | no |
+| configurationSettingsOverride | Optional configuration settings to override default IoT Operations extension configuration. Use the same key names as the az iot ops --ops-config parameter. | `object` | {} | no |
+| schemaRegistryName | The resource name for the ADR Schema Registry for Azure IoT Operations. | `string` | n/a | yes |
+| adrNamespaceName | The resource name for the ADR Namespace for Azure IoT Operations. | `string` | n/a | no |
+| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no |
+| shouldDeployResourceSyncRules | Whether or not to deploy the Custom Locations Resource Sync Rules for the Azure IoT Operations resources. | `bool` | `true` | no |
+| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `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 for Azure IoT Operations. | `bool` | `true` | no |
+| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no |
+| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no |
+| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no |
+| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no |
+| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no |
+| akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no |
+| customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no |
+| additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no |
+| trustIssuerSettings | The trust issuer settings for Customer Managed Azure IoT Operations Settings. | `[_1.TrustIssuerConfig](#user-defined-types)` | {'trustSource': 'SelfSigned'} | no |
+| sseKeyVaultName | The name of the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | n/a | yes |
+| sseIdentityName | The name of the User Assigned Managed Identity for Secret Sync. | `string` | n/a | yes |
+| sseKeyVaultResourceGroupName | The name of the Resource Group for the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | [resourceGroup().name] | no |
+| shouldAssignSseKeyVaultRoles | Whether to assign roles for Key Vault to the provided Secret Sync Identity. | `bool` | `true` | no |
+| shouldAssignDeployIdentityRoles | Whether to assign roles to the deploy identity. | `bool` | [not(empty(parameters('deployIdentityName')))] | no |
+| deployIdentityName | The resource name for a managed identity that will be given deployment admin permissions. | `string` | n/a | no |
+| shouldDeployAioDeploymentScripts | Whether to deploy DeploymentScripts for Azure IoT Operations. | `bool` | `false` | no |
+| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | [parameters('sseKeyVaultName')] | no |
+| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [parameters('sseKeyVaultResourceGroupName')] | no |
+| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | deploy-user-token | no |
+| deploymentScriptsSecretNamePrefix | The prefix used with constructing the secret name that will have the deployment script. | `string` | [format('{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no |
+| shouldAddDeployScriptsToKeyVault | Whether to add the deploy scripts for DeploymentScripts to Key Vault as secrets. (Required for DeploymentScripts) | `bool` | `false` | no |
+| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no |
## Resources
@@ -75,7 +73,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure
| deployArcK8sRoleAssignments | Assigns required Azure Arc roles to the deployment identity for cluster access. |
| deployKeyVaultRoleAssignments | Assigns required Key Vault roles to the deployment identity for script execution. |
| sseKeyVaultRoleAssignments | Assigns roles for Secret Sync to access Key Vault. |
-| iotOpsInit | Initializes and configures the required Arc extensions for Azure IoT Operations including Secret Store, Open Service Mesh, Container Storage, and IoT Operations Platform. |
+| iotOpsInit | Initializes and configures the Secret Store extension for Azure IoT Operations. Depends on cert-manager deployed via 109-arc-extensions component. |
| postInitScriptsSecrets | Creates secrets in Key Vault for deployment script setup and initialization for Azure IoT Operations. |
| postInitScripts | Runs deployment scripts for IoT Operations using an Azure deploymentScript resource, including tool installation and script execution. |
| iotOpsInstance | Deploys Azure IoT Operations instance, broker, authentication, listeners, and data flow components on an Azure Arc-enabled Kubernetes cluster. |
@@ -160,36 +158,27 @@ Assigns roles for Secret Sync to access Key Vault.
### iotOpsInit
-Initializes and configures the required Arc extensions for Azure IoT Operations including Secret Store, Open Service Mesh, Container Storage, and IoT Operations Platform.
+Initializes and configures the Secret Store extension for Azure IoT Operations. Depends on cert-manager deployed via 109-arc-extensions component.
#### Parameters for iotOpsInit
-| Name | Description | Type | Default | Required |
-|:------------------------|:------------------------------------------------------------------------------|:------------------------------------------------------|:--------|:---------|
-| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes |
-| containerStorageConfig | The settings for the Azure Container Store for Azure Arc Extension. | `[_1.ContainerStorageExtension](#user-defined-types)` | n/a | yes |
-| aioCertManagerConfig | The settings for the Azure IoT Operations Platform Extension. | `[_1.AioCertManagerExtension](#user-defined-types)` | n/a | yes |
-| secretStoreConfig | The settings for the Secret Store Extension. | `[_1.SecretStoreExtension](#user-defined-types)` | n/a | yes |
-| trustIssuerSettings | The trust issuer settings for Customer Managed Azure IoT Operations Settings. | `[_1.TrustIssuerConfig](#user-defined-types)` | n/a | yes |
+| Name | Description | Type | Default | Required |
+|:------------------------|:-------------------------------------------------|:-------------------------------------------------|:--------|:---------|
+| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes |
+| secretStoreConfig | The settings for the Secret Store Extension. | `[_1.SecretStoreExtension](#user-defined-types)` | n/a | yes |
#### Resources for iotOpsInit
-| Name | Type | API Version |
-|:-----------------|:-----------------------------------------------|:------------|
-| aioCertManager | `Microsoft.KubernetesConfiguration/extensions` | 2023-05-01 |
-| containerStorage | `Microsoft.KubernetesConfiguration/extensions` | 2023-05-01 |
-| secretStore | `Microsoft.KubernetesConfiguration/extensions` | 2023-05-01 |
+| Name | Type | API Version |
+|:------------|:-----------------------------------------------|:------------|
+| secretStore | `Microsoft.KubernetesConfiguration/extensions` | 2024-11-01 |
#### Outputs for iotOpsInit
-| Name | Type | Description |
-|:------------------------------|:---------|:-------------------------------------------------------------|
-| containerStorageExtensionId | `string` | The ID of the Container Storage Extension. |
-| containerStorageExtensionName | `string` | The name of the Container Storage Extension. |
-| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. |
-| secretStoreExtensionName | `string` | The name of the Secret Store Extension. |
-| aioCertManagerExtensionId | `string` | The ID of the Azure IoT Operations Cert-Manager Extension. |
-| aioCertManagerExtensionName | `string` | The name of the Azure IoT Operations Cert-Manager Extension. |
+| Name | Type | Description |
+|:-------------------------|:---------|:----------------------------------------|
+| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. |
+| secretStoreExtensionName | `string` | The name of the Secret Store Extension. |
### postInitScriptsSecrets
@@ -298,7 +287,7 @@ Deploys Azure IoT Operations instance, broker, authentication, listeners, and da
|:--------------------------------------|:--------------------------------------------------------------------------------|:-------------------|
| sseIdentity::sseFedCred | `Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials` | 2023-01-31 |
| aioIdentity::aioFedCred | `Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials` | 2023-01-31 |
-| aioExtension | `Microsoft.KubernetesConfiguration/extensions` | 2023-05-01 |
+| aioExtension | `Microsoft.KubernetesConfiguration/extensions` | 2024-11-01 |
| aioExtensionSchemaRegistryContributor | `Microsoft.Authorization/roleAssignments` | 2022-04-01 |
| customLocation | `Microsoft.ExtendedLocation/customLocations` | 2021-08-31-preview |
| aioSyncRule | `Microsoft.ExtendedLocation/customLocations/resourceSyncRules` | 2021-08-31-preview |
@@ -445,15 +434,6 @@ Configuration for Azure IoT Operations Certificate Authority.
| caCertChainPem | `securestring` | The PEM-formatted CA certificate chain. |
| caKeyPem | `securestring` | The PEM-formatted CA private key. |
-### `_1.AioCertManagerExtension`
-
-The settings for the Azure IoT Operations Platform Extension.
-
-| Property | Type | Description |
-|:---------|:------------------------------------|:---------------------------------------|
-| release | `[_1.Release](#user-defined-types)` | The common settings for the extension. |
-| settings | `object` | |
-
### `_1.AioDataFlowInstance`
The settings for Azure IoT Operations Data Flow Instances.
@@ -584,15 +564,6 @@ Broker persistence configuration for disk-backed message storage.
| subscriberQueue | `object` | Controls which subscriber queues should be persisted to disk. |
| persistentVolumeClaimSpec | `object` | Persistent volume claim specification for storage. |
-### `_1.ContainerStorageExtension`
-
-The settings for the Azure Container Store for Azure Arc Extension.
-
-| Property | Type | Description |
-|:---------|:------------------------------------|:---------------------------------------|
-| release | `[_1.Release](#user-defined-types)` | The common settings for the extension. |
-| settings | `object` | |
-
### `_1.CustomerManagedByoIssuerConfig`
The configuration for Customer Managed Bring Your Own Issuer for Azure IoT Operations certificates.
@@ -726,23 +697,21 @@ Common settings for the components.
## Outputs
-| Name | Type | Description |
-|:------------------------------|:---------|:-------------------------------------------------------------------|
-| containerStorageExtensionId | `string` | The ID of the Container Storage Extension. |
-| containerStorageExtensionName | `string` | The name of the Container Storage Extension. |
-| aioCertManagerExtensionId | `string` | The ID of the Azure IoT Operations Cert-Manager Extension. |
-| aioCertManagerExtensionName | `string` | The name of the Azure IoT Operations Cert-Manager Extension. |
-| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. |
-| secretStoreExtensionName | `string` | The name of the Secret Store Extension. |
-| customLocationId | `string` | The ID of the deployed Custom Location. |
-| customLocationName | `string` | The name of the deployed Custom Location. |
-| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. |
-| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. |
-| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. |
-| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. |
-| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. |
-| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. |
-| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. |
-| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. |
+| Name | Type | Description |
+|:---------------------------|:---------|:-------------------------------------------------------------------|
+| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. |
+| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. |
+| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. |
+| secretStoreExtensionName | `string` | The name of the Secret Store Extension. |
+| customLocationId | `string` | The ID of the deployed Custom Location. |
+| customLocationName | `string` | The name of the deployed Custom Location. |
+| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. |
+| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. |
+| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. |
+| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. |
+| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. |
+| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. |
+| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. |
+| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. |
\ No newline at end of file
diff --git a/src/100-edge/110-iot-ops/bicep/main.bicep b/src/100-edge/110-iot-ops/bicep/main.bicep
index ac776c2b..07089eb0 100644
--- a/src/100-edge/110-iot-ops/bicep/main.bicep
+++ b/src/100-edge/110-iot-ops/bicep/main.bicep
@@ -18,12 +18,6 @@ param arcConnectedClusterName string
Azure IoT Operations Init Parameters
*/
-@description('The settings for the Azure Container Store for Azure Arc Extension.')
-param containerStorageConfig types.ContainerStorageExtension = types.containerStorageExtensionDefaults
-
-@description('The settings for the Azure IoT Operations Platform Extension.')
-param aioCertManagerConfig types.AioCertManagerExtension = types.aioCertManagerExtensionDefaults
-
@description('The settings for the Secret Store Extension.')
#disable-next-line secure-secrets-in-params
param secretStoreConfig types.SecretStoreExtension = types.secretStoreExtensionDefaults
@@ -288,11 +282,8 @@ module iotOpsInit 'modules/iot-ops-init.bicep' = if (shouldInitAio) {
deployKeyVaultRoleAssignments
]
params: {
- aioCertManagerConfig: aioCertManagerConfig
arcConnectedClusterName: arcConnectedClusterName
- containerStorageConfig: containerStorageConfig
secretStoreConfig: secretStoreConfig
- trustIssuerSettings: trustIssuerSettings
}
}
@@ -432,17 +423,11 @@ module postInstanceScripts 'modules/apply-scripts.bicep' = if (shouldDeployAioDe
Outputs
*/
-@description('The ID of the Container Storage Extension.')
-output containerStorageExtensionId string = (iotOpsInit.?outputs.?containerStorageExtensionId) ?? ''
-
-@description('The name of the Container Storage Extension.')
-output containerStorageExtensionName string = (iotOpsInit.?outputs.?containerStorageExtensionName) ?? ''
-
-@description('The ID of the Azure IoT Operations Cert-Manager Extension.')
-output aioCertManagerExtensionId string = (iotOpsInit.?outputs.?aioCertManagerExtensionId) ?? ''
+@description('The ID of the Azure IoT Operations Platform Extension.')
+output aioPlatformExtensionId string = shouldDeployAio ? (iotOpsInstance.?outputs.?aioExtensionId ?? '') : ''
-@description('The name of the Azure IoT Operations Cert-Manager Extension.')
-output aioCertManagerExtensionName string = (iotOpsInit.?outputs.?aioCertManagerExtensionName) ?? ''
+@description('The name of the Azure IoT Operations Platform Extension.')
+output aioPlatformExtensionName string = shouldDeployAio ? (iotOpsInstance.?outputs.?aioExtensionName ?? '') : ''
@description('The ID of the Secret Store Extension.')
output secretStoreExtensionId string = (iotOpsInit.?outputs.?secretStoreExtensionId) ?? ''
diff --git a/src/100-edge/110-iot-ops/bicep/modules/iot-ops-init.bicep b/src/100-edge/110-iot-ops/bicep/modules/iot-ops-init.bicep
index 1b5d1e56..fccd8a25 100644
--- a/src/100-edge/110-iot-ops/bicep/modules/iot-ops-init.bicep
+++ b/src/100-edge/110-iot-ops/bicep/modules/iot-ops-init.bicep
@@ -1,5 +1,5 @@
metadata name = 'IoT Operations Initialization Module'
-metadata description = 'Initializes and configures the required Arc extensions for Azure IoT Operations including Secret Store, Open Service Mesh, Container Storage, and IoT Operations Platform.'
+metadata description = 'Initializes and configures the Secret Store extension for Azure IoT Operations. Depends on cert-manager deployed via 109-arc-extensions component.'
import * as types from '../types.bicep'
@@ -10,39 +10,10 @@ import * as types from '../types.bicep'
@description('The resource name for the Arc connected cluster.')
param arcConnectedClusterName string
-@description('The settings for the Azure Container Store for Azure Arc Extension.')
-param containerStorageConfig types.ContainerStorageExtension
-
-@description('The settings for the Azure IoT Operations Platform Extension.')
-param aioCertManagerConfig types.AioCertManagerExtension
-
@description('The settings for the Secret Store Extension.')
#disable-next-line secure-secrets-in-params
param secretStoreConfig types.SecretStoreExtension
-@description('The trust issuer settings for Customer Managed Azure IoT Operations Settings.')
-param trustIssuerSettings types.TrustIssuerConfig
-
-/*
- Variables
-*/
-
-// Setup ACSA StorageClass based on either provided StorageClass, Fault Tolerance Enabled, or the
-// default StorageClass provided by local-path.
-var defaultStorageClass = containerStorageConfig.settings.faultToleranceEnabled
- ? 'acstor-arccontainerstorage-storage-pool'
- : 'default,local-path'
-
-var kubernetesStorageClass = containerStorageConfig.settings.?diskStorageClass ?? defaultStorageClass
-var diskMountPoint = containerStorageConfig.settings.?diskMountPoint ?? '/mnt'
-
-var faultToleranceConfig = containerStorageConfig.settings.faultToleranceEnabled
- ? {
- 'acstorConfiguration.create': 'true'
- 'acstorConfiguration.properties.diskMountPoint': diskMountPoint
- }
- : {}
-
/*
Resources
*/
@@ -51,53 +22,7 @@ resource arcConnectedCluster 'Microsoft.Kubernetes/connectedClusters@2021-03-01'
name: arcConnectedClusterName
}
-resource aioCertManager 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' = if (trustIssuerSettings.trustSource != 'CustomerManagedByoIssuer') {
- scope: arcConnectedCluster
- name: 'cert-manager'
- identity: {
- type: 'SystemAssigned'
- }
- properties: {
- extensionType: 'microsoft.certmanagement'
- version: aioCertManagerConfig.release.version
- releaseTrain: aioCertManagerConfig.release.train
- autoUpgradeMinorVersion: false
- scope: {
- cluster: {
- releaseNamespace: 'cert-manager'
- }
- }
- configurationSettings: {
- AgentOperationTimeoutInMinutes: aioCertManagerConfig.settings.agentOperationTimeoutInMinutes
- 'global.telemetry.enabled': '${aioCertManagerConfig.settings.?globalTelemetryEnabled} ?? true'
- }
- }
-}
-
-resource containerStorage 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' = {
- scope: arcConnectedCluster
- // 'azure-arc-containerstorage' is the required extension name for ACSA.
- name: 'azure-arc-containerstorage'
- identity: {
- type: 'SystemAssigned'
- }
- properties: {
- extensionType: 'microsoft.arc.containerstorage'
- autoUpgradeMinorVersion: false
- version: containerStorageConfig.release.version
- releaseTrain: containerStorageConfig.release.train
- configurationSettings: {
- 'edgeStorageConfiguration.create': 'true'
- 'feature.diskStorageClass': kubernetesStorageClass
- ...faultToleranceConfig
- }
- }
- dependsOn: [
- aioCertManager
- ]
-}
-
-resource secretStore 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' = {
+resource secretStore 'Microsoft.KubernetesConfiguration/extensions@2024-11-01' = {
scope: arcConnectedCluster
// 'azure-secret-store' is the required extension name for SSE.
name: 'azure-secret-store'
@@ -114,29 +39,14 @@ resource secretStore 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' =
'validatingAdmissionPolicies.applyPolicies': 'false'
}
}
- dependsOn: [
- aioCertManager
- ]
}
/*
Outputs
*/
-@description('The ID of the Container Storage Extension.')
-output containerStorageExtensionId string = containerStorage.id
-
-@description('The name of the Container Storage Extension.')
-output containerStorageExtensionName string = containerStorage.name
-
@description('The ID of the Secret Store Extension.')
output secretStoreExtensionId string = secretStore.id
@description('The name of the Secret Store Extension.')
output secretStoreExtensionName string = secretStore.name
-
-@description('The ID of the Azure IoT Operations Cert-Manager Extension.')
-output aioCertManagerExtensionId string = aioCertManager.id
-
-@description('The name of the Azure IoT Operations Cert-Manager Extension.')
-output aioCertManagerExtensionName string = aioCertManager.name
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 ca26e9ec..91a40978 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
@@ -157,7 +157,7 @@ resource arcConnectedCluster 'Microsoft.Kubernetes/connectedClusters@2024-12-01-
name: arcConnectedClusterName
}
-resource aioExtension 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' = {
+resource aioExtension 'Microsoft.KubernetesConfiguration/extensions@2024-11-01' = {
scope: arcConnectedCluster
name: 'azure-iot-operations-${take(uniqueString(arcConnectedCluster.id), 5)}'
identity: {
diff --git a/src/100-edge/110-iot-ops/bicep/types.bicep b/src/100-edge/110-iot-ops/bicep/types.bicep
index f3256769..8a49929f 100644
--- a/src/100-edge/110-iot-ops/bicep/types.bicep
+++ b/src/100-edge/110-iot-ops/bicep/types.bicep
@@ -1,9 +1,10 @@
/*
- * IMPORTANT: The variable names in this file ('aioCertManagerExtensionDefaults',
- * 'secretStoreExtensionDefaults', 'containerStorageExtensionDefaults',
+ * IMPORTANT: The variable names in this file ('secretStoreExtensionDefaults',
* 'aioExtensionDefaults') are explicitly referenced
* by the aio-version-checker.py script. If you rename these variables or change their structure,
* you must also update the script and the aio-version-checker-template.yml pipeline.
+ * NOTE: 'aioCertManagerExtensionDefaults' and 'containerStorageExtensionDefaults' have been
+ * moved to the 109-arc-extensions component.
*/
@export()
@@ -31,61 +32,6 @@ var secretStoreExtensionDefaults = {
}
}
-@export()
-@description('The settings for the Azure Container Store for Azure Arc Extension.')
-type ContainerStorageExtension = {
- @description('The common settings for the extension.')
- release: Release
-
- settings: {
- @description('Whether or not to enable fault tolerant storage in the cluster.')
- faultToleranceEnabled: bool
-
- @description('The storage class for the cluster. (Otherwise, "acstor-arccontainerstorage-storage-pool" for fault tolerant storage else "default,local-path")')
- diskStorageClass: string?
-
- @description('The disk mount point for the cluster when using fault tolerant storage. (Otherwise: "/mnt" when using fault tolerant storage)')
- diskMountPoint: string?
- }
-}
-
-@export()
-var containerStorageExtensionDefaults = {
- release: {
- version: '2.6.0'
- train: 'stable'
- }
- settings: {
- faultToleranceEnabled: false
- }
-}
-
-@export()
-@description('The settings for the Azure IoT Operations Platform Extension.')
-type AioCertManagerExtension = {
- @description('The common settings for the extension.')
- release: Release
-
- settings: {
- @description('Agent operation timeout in minutes.')
- agentOperationTimeoutInMinutes: string
- @description('Enable or disable global telemetry.')
- globalTelemetryEnabled: bool?
- }
-}
-
-@export()
-var aioCertManagerExtensionDefaults = {
- release: {
- version: '0.7.0'
- train: 'stable'
- }
- settings: {
- agentOperationTimeoutInMinutes: '20'
- globalTelemetryEnabled: true
- }
-}
-
@export()
@description('The settings for the Azure IoT Operations Extension.')
type AioExtension = {
diff --git a/src/100-edge/110-iot-ops/scripts/init-scripts.sh b/src/100-edge/110-iot-ops/scripts/init-scripts.sh
index 64f8d9f3..fa61c37c 100755
--- a/src/100-edge/110-iot-ops/scripts/init-scripts.sh
+++ b/src/100-edge/110-iot-ops/scripts/init-scripts.sh
@@ -1,14 +1,74 @@
#!/usr/bin/env bash
+# Azure Arc-enabled Kubernetes Proxy Initialization Script
+#
+# This script establishes connectivity to an Azure Arc-enabled Kubernetes cluster
+# by managing the az connectedk8s proxy lifecycle and ensuring the AIO namespace exists.
+#
+# Key Features:
+# - Detects existing kubectl connectivity before starting a new proxy
+# - Manages az connectedk8s proxy with proper cleanup on exit/interrupt
+# - Implements race condition fix for kubeconfig file creation
+# - Creates AIO namespace if not present
+#
+# Race Condition Fix:
+# The az connectedk8s proxy writes kubeconfig asynchronously in the background.
+# To prevent partial/incomplete reads, the proxy writes to a temporary file first.
+# A wrapper process monitors the temp file, waits for it to be complete, then
+# atomically moves it to the final location. This ensures the kubeconfig file
+# only appears when fully written and ready for use.
+#
+# Required Environment Variables:
+# - TF_CONNECTED_CLUSTER_NAME: Name of the Arc-enabled Kubernetes cluster
+# - TF_RESOURCE_GROUP_NAME: Azure resource group containing the cluster
+# - TF_AIO_NAMESPACE: Namespace to create/ensure exists
+# - TF_MODULE_PATH: Path to module containing YAML resources
+#
+# Optional Environment Variables:
+# - DEPLOY_USER_TOKEN_SECRET: Key Vault secret name for deploy token
+# - DEPLOY_KEY_VAULT_NAME: Key Vault name for deploy token retrieval
+# (both must be set together if using token-based authentication)
+
# Set error handling to continue on errors
set +e
+# Validate required environment variables
+required_vars=(
+ "TF_CONNECTED_CLUSTER_NAME"
+ "TF_RESOURCE_GROUP_NAME"
+ "TF_AIO_NAMESPACE"
+ "TF_MODULE_PATH"
+)
+
+missing_vars=()
+for var in "${required_vars[@]}"; do
+ if [[ -z "${!var}" ]]; then
+ missing_vars+=("$var")
+ fi
+done
+
+if [ ${#missing_vars[@]} -gt 0 ]; then
+ echo "ERROR: Required environment variables not set:" >&2
+ printf " - %s\n" "${missing_vars[@]}" >&2
+ exit 1
+fi
+
+# Validate optional token variables are both set or both unset
+if [[ -n "${DEPLOY_USER_TOKEN_SECRET}" && -z "${DEPLOY_KEY_VAULT_NAME}" ]]; then
+ echo "ERROR: DEPLOY_USER_TOKEN_SECRET is set but DEPLOY_KEY_VAULT_NAME is not" >&2
+ exit 1
+elif [[ -z "${DEPLOY_USER_TOKEN_SECRET}" && -n "${DEPLOY_KEY_VAULT_NAME}" ]]; then
+ echo "ERROR: DEPLOY_KEY_VAULT_NAME is set but DEPLOY_USER_TOKEN_SECRET is not" >&2
+ exit 1
+fi
+
# Function to clean up resources
cleanup() {
local exit_code=$?
echo "Cleaning up..."
[ -f "$kube_config_file" ] && rm "$kube_config_file" && echo "Deleted kubeconfig file"
+ [ -f "${kube_config_temp:-}" ] && rm "$kube_config_temp" && echo "Deleted temporary kubeconfig file"
# Kill the proxy process group
if [[ ${proxy_pid:-} ]]; then
@@ -50,7 +110,13 @@ cleanup() {
}
check_connected_to_cluster() {
- if connected_to_cluster=$(kubectl get cm azure-clusterconfig -n azure-arc -o jsonpath="{.data.AZURE_RESOURCE_NAME}" --kubeconfig "$kube_config_file" 2>/dev/null); then
+ # Check if kubeconfig file exists and has already been populated by az connectedk8s proxy running in background
+ if [[ ! -s "$kube_config_file" ]]; then
+ return 1
+ fi
+
+ # Verify connectivity and cluster identity
+ if connected_to_cluster=$(kubectl get cm azure-clusterconfig -n azure-arc -o jsonpath="{.data.AZURE_RESOURCE_NAME}" --kubeconfig "$kube_config_file" --request-timeout=10s 2>/dev/null); then
if [ "$connected_to_cluster" == "$TF_CONNECTED_CLUSTER_NAME" ]; then
return 0
fi
@@ -60,14 +126,24 @@ check_connected_to_cluster() {
start_proxy() {
# Use a custom kubeconfig file to ensure the current user's context is not affected
- kube_config_file=$(mktemp -t "${TF_CONNECTED_CLUSTER_NAME}.XXX")
- # Start proxy in its own process group with -m
- set -m
+ if ! kube_config_file=$(mktemp -t "${TF_CONNECTED_CLUSTER_NAME}.XXX"); then
+ echo "ERROR: Failed to create temporary kubeconfig file" >&2
+ exit 1
+ fi
+
+ # Race condition fix: az connectedk8s proxy writes to temp file first, then atomically moved to final location
+ # This ensures kubeconfig file only has non-empty content when fully written, avoiding partial/incomplete reads
+ if ! kube_config_temp=$(mktemp -t "${TF_CONNECTED_CLUSTER_NAME}.temp.XXX"); then
+ echo "ERROR: Failed to create secondary temporary kubeconfig file" >&2
+ exit 1
+ fi
+
+ # Build proxy arguments
local -a proxy_args=(
"-n" "$TF_CONNECTED_CLUSTER_NAME"
"-g" "$TF_RESOURCE_GROUP_NAME"
"--port" "9800"
- "--file" "$kube_config_file"
+ "--file" "$kube_config_temp"
)
local deploy_user_token=""
if [[ $DEPLOY_USER_TOKEN_SECRET ]]; then
@@ -83,7 +159,51 @@ start_proxy() {
echo "Got Deploy User Token..."
proxy_args+=("--token" "$deploy_user_token")
fi
- az connectedk8s proxy "${proxy_args[@]}" >/dev/null &
+
+ # Start proxy wrapper in its own process group
+ set -m
+ {
+ # Start az connectedk8s proxy writing to temp file
+ az connectedk8s proxy "${proxy_args[@]}" >/dev/null &
+ az_pid=$!
+
+ # Wait for temp file to have content
+ local wait_count=0
+ while [[ ! -s "$kube_config_temp" ]]; do
+ if ! kill -0 "$az_pid" 2>/dev/null; then
+ echo "ERROR: az connectedk8s proxy exited unexpectedly" >&2
+ kill "$az_pid" 2>/dev/null
+ # Signal parent to trigger cleanup and exit
+ kill -INT $$ 2>/dev/null
+ exit 1
+ fi
+ sleep 0.5
+ ((wait_count += 1))
+ if [ "$wait_count" -gt 60 ]; then
+ echo "ERROR: timeout waiting for kubeconfig file creation" >&2
+ kill "$az_pid" 2>/dev/null
+ # Signal parent to trigger cleanup and exit
+ kill -INT $$ 2>/dev/null
+ exit 1
+ fi
+ done
+
+ # Give az connectedk8s proxy time to finish writing
+ sleep 2
+
+ # Atomically move temp file to final location
+ if ! mv "$kube_config_temp" "$kube_config_file"; then
+ echo "ERROR: Failed to move kubeconfig file from temp location" >&2
+ kill "$az_pid" 2>/dev/null
+ # Signal parent to trigger cleanup and exit
+ kill -INT $$ 2>/dev/null
+ exit 1
+ fi
+
+ # Keep az proxy running in foreground of this subshell
+ wait "$az_pid" || exit 1
+ } &
+
export proxy_pid=$!
proxy_pgid=$(ps -o pgid= -p "$proxy_pid" 2>/dev/null | tr -d ' ')
if [[ ! $proxy_pgid ]]; then
@@ -96,6 +216,10 @@ start_proxy() {
timeout=0
until check_connected_to_cluster; do
+ if ! kill -0 "$proxy_pid" 2>/dev/null; then
+ echo "ERROR: az connectedk8s proxy wrapper exited unexpectedly" >&2
+ return 1
+ fi
sleep 1
((timeout += 1))
if [ "$timeout" -gt 30 ]; then
@@ -121,14 +245,21 @@ else
echo "Starting 'az connectedk8s proxy'"
- start_proxy
+ start_proxy || exit 1
fi
# Ensure aio namespace is created and exists
-if ! kubectl get namespace "$TF_AIO_NAMESPACE" --kubeconfig "$kube_config_file"; then
+if ! kubectl get namespace "$TF_AIO_NAMESPACE" --kubeconfig "$kube_config_file" &>/dev/null; then
+ echo "Namespace $TF_AIO_NAMESPACE not found, attempting to create..."
+ timeout=0
until envsubst <"$TF_MODULE_PATH/yaml/aio-namespace.yaml" | kubectl apply -f - --kubeconfig "$kube_config_file"; do
- echo "Error applying aio-namespace.yaml, retrying in 5 seconds"
+ echo "Error applying aio-namespace.yaml, retrying in 5 seconds..."
sleep 5
+ ((timeout += 5))
+ if [ "$timeout" -gt 60 ]; then
+ echo "ERROR: timed out creating namespace $TF_AIO_NAMESPACE" >&2
+ exit 1
+ fi
done
fi
diff --git a/src/100-edge/110-iot-ops/terraform/README.md b/src/100-edge/110-iot-ops/terraform/README.md
index a878654d..93d1c777 100644
--- a/src/100-edge/110-iot-ops/terraform/README.md
+++ b/src/100-edge/110-iot-ops/terraform/README.md
@@ -40,15 +40,12 @@ Instance can be created, and after.
| secret\_sync\_key\_vault | Azure Key Vault ID to use with Secret Sync Extension. | ```object({ name = string id = string })``` | n/a | yes |
| additional\_cluster\_extension\_ids | Additional cluster extension IDs to include in the custom location. Appended to the default Secret Store and IoT Operations extension IDs | `list(string)` | `[]` | no |
| aio\_ca | CA certificate for the MQTT broker, can be either Root CA or Root CA with any number of Intermediate CAs. If not provided, a self-signed Root CA with a intermediate will be generated. Only valid when Trust Source is set to CustomerManaged | ```object({ root_ca_cert_pem = string ca_cert_chain_pem = string ca_key_pem = string })``` | `null` | no |
-| aio\_cert\_manager\_config | Install cert-manager | ```object({ agent_operation_timeout_in_minutes = string global_telemetry_enabled = bool })``` | ```{ "agent_operation_timeout_in_minutes": "20", "global_telemetry_enabled": true }``` | no |
| aio\_features | AIO Instance features with mode ('Stable', 'Preview', 'Disabled') and settings ('Enabled', 'Disabled'). | ```map(object({ mode = optional(string) settings = optional(map(string)) }))``` | `null` | no |
| broker\_listener\_anonymous\_config | Configuration for the insecure anonymous AIO MQ Broker Listener. For additional information, refer to: | ```object({ serviceName = string port = number nodePort = number })``` | ```{ "nodePort": 31884, "port": 18884, "serviceName": "aio-broker-anon" }``` | no |
| byo\_issuer\_trust\_settings | Settings for CustomerManagedByoIssuer (Bring Your Own Issuer) trust configuration | ```object({ issuer_name = string issuer_kind = string configmap_name = string configmap_key = string })``` | `null` | no |
-| cert\_manager | n/a | ```object({ version = string train = string })``` | ```{ "train": "stable", "version": "0.7.0" }``` | no |
| configuration\_settings\_override | Optional configuration settings to override default IoT Operations extension configuration. Use the same key names as the az iot ops --ops-config parameter. | `map(string)` | `{}` | no |
| custom\_akri\_connectors | List of custom Akri connector templates with user-defined endpoint types and container images. Supports built-in types (rest, media, onvif, sse) or custom types with custom\_endpoint\_type and custom\_image\_name. Built-in connectors default to mcr.microsoft.com/azureiotoperations/akri-connectors/connector\_type:0.5.1. Examples: # ONVIF Camera Connector (Built-in) custom\_akri\_connectors = [ { name = "warehouse-camera-connector" type = "onvif" replicas = 2 log\_level = "info" } ] # SSE Event Connector (Built-in) custom\_akri\_connectors = [ { name = "analytics-camera-connector" type = "sse" replicas = 1 log\_level = "info" } ] # REST API Connector (Built-in) custom\_akri\_connectors = [ { name = "sensor-api-connector" type = "rest" replicas = 1 log\_level = "info" } ] # Custom Modbus Connector custom\_akri\_connectors = [ { name = "modbus-telemetry-connector" type = "custom" custom\_endpoint\_type = "Contoso.Modbus" custom\_image\_name = "my\_acr.azurecr.io/modbus-telemetry-connector" custom\_endpoint\_version = "2.0" registry = "my\_acr.azurecr.io" image\_tag = "v1.2.3" replicas = 2 log\_level = "debug" } ] # Multiple Connectors with MQTT Override custom\_akri\_connectors = [ { name = "warehouse-ptz-cameras" type = "onvif" replicas = 3 log\_level = "info" mqtt\_config = { host = "aio-broker.azure-iot-operations" audience = "aio-broker" ca\_configmap = "aio-ca-trust-bundle" keep\_alive\_seconds = 60 max\_inflight\_messages = 100 session\_expiry\_seconds = 600 } }, { name = "analytics-event-stream" type = "sse" replicas = 2 log\_level = "debug" } ] | ```list(object({ name = string type = string // "rest", "media", "onvif", "sse", "custom" // Custom Connector Fields (required when type = "custom") custom_endpoint_type = optional(string) // e.g., "Contoso.Modbus", "Acme.CustomProtocol" custom_image_name = optional(string) // e.g., "my_acr.azurecr.io/custom-connector" custom_endpoint_version = optional(string, "1.0") // Runtime Configuration (defaults applied based on connector type) registry = optional(string) // Defaults: mcr.microsoft.com for built-in types image_tag = optional(string) // Defaults: 0.5.1 for built-in types, latest for custom replicas = optional(number, 1) image_pull_policy = optional(string) // Default: IfNotPresent // Diagnostics log_level = optional(string) // Default: info (lowercase: trace, debug, info, warning, error, critical) // MQTT Override (uses shared config if not provided) mqtt_config = optional(object({ host = string audience = string ca_configmap = string keep_alive_seconds = optional(number, 60) max_inflight_messages = optional(number, 100) session_expiry_seconds = optional(number, 600) })) // Optional Advanced Fields aio_min_version = optional(string, "1.2.37") aio_max_version = optional(string) allocation = optional(object({ policy = string // "Bucketized" bucket_size = number // 1-100 })) additional_configuration = optional(map(string)) secrets = optional(list(object({ secret_alias = string secret_key = string secret_ref = string }))) trust_settings = optional(object({ trust_list_secret_ref = string })) }))``` | `[]` | no |
| dataflow\_instance\_count | Number of dataflow instances. Defaults to 1. | `number` | `1` | no |
-| edge\_storage\_accelerator | n/a | ```object({ version = string train = string diskStorageClass = string faultToleranceEnabled = bool diskMountPoint = string })``` | ```{ "diskMountPoint": "/mnt", "diskStorageClass": "", "faultToleranceEnabled": false, "train": "stable", "version": "2.6.0" }``` | no |
| enable\_instance\_secret\_sync | Whether to enable secret sync on the Azure IoT Operations instance | `bool` | `true` | no |
| enable\_opc\_ua\_simulator | Deploy OPC UA Simulator to the cluster | `bool` | `true` | no |
| mqtt\_broker\_config | n/a | ```object({ brokerListenerServiceName = string brokerListenerPort = number serviceAccountAudience = string frontendReplicas = number frontendWorkers = number backendRedundancyFactor = number backendWorkers = number backendPartitions = number memoryProfile = string serviceType = string logsLevel = optional(string, "info") })``` | ```{ "backendPartitions": 2, "backendRedundancyFactor": 2, "backendWorkers": 2, "brokerListenerPort": 18883, "brokerListenerServiceName": "aio-broker", "frontendReplicas": 2, "frontendWorkers": 2, "logsLevel": "info", "memoryProfile": "Medium", "serviceAccountAudience": "aio-internal", "serviceType": "ClusterIp" }``` | no |
diff --git a/src/100-edge/110-iot-ops/terraform/main.tf b/src/100-edge/110-iot-ops/terraform/main.tf
index 5622a579..8255b86a 100644
--- a/src/100-edge/110-iot-ops/terraform/main.tf
+++ b/src/100-edge/110-iot-ops/terraform/main.tf
@@ -65,10 +65,6 @@ module "iot_ops_init" {
depends_on = [module.role_assignments]
arc_connected_cluster_id = var.arc_connected_cluster.id
- trust_config_source = var.trust_config_source
- aio_cert_manager_config = var.aio_cert_manager_config
- cert_manager = var.cert_manager
- edge_storage_accelerator = var.edge_storage_accelerator
secret_sync_controller = var.secret_sync_controller
resource_group = var.resource_group
connected_cluster_name = var.arc_connected_cluster.name
diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/README.md b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/README.md
index af0314d4..ab0ffc71 100644
--- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/README.md
+++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/README.md
@@ -20,8 +20,6 @@ Deploys resources necessary to enable Azure IoT Operations (AIO) and creates an
| Name | Type |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
-| [azurerm_arc_kubernetes_cluster_extension.cert_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/arc_kubernetes_cluster_extension) | resource |
-| [azurerm_arc_kubernetes_cluster_extension.container_storage](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/arc_kubernetes_cluster_extension) | resource |
| [azurerm_arc_kubernetes_cluster_extension.secret_store](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/arc_kubernetes_cluster_extension) | resource |
| [azurerm_federated_identity_credential.federated_identity_cred_aio_instance](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | resource |
| [azurerm_federated_identity_credential.federated_identity_cred_sse_aio](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | resource |
@@ -29,20 +27,16 @@ Deploys resources necessary to enable Azure IoT Operations (AIO) and creates an
## Inputs
-| Name | Description | Type | Default | Required |
-|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|---------|:--------:|
-| aio\_cert\_manager\_config | Install cert-manager and trust-manager extensions | ```object({ agent_operation_timeout_in_minutes = string global_telemetry_enabled = bool })``` | n/a | yes |
-| aio\_namespace | Azure IoT Operations namespace | `string` | n/a | yes |
-| aio\_user\_managed\_identity\_id | ID of the User Assigned Managed Identity for the Azure IoT Operations instance | `string` | n/a | yes |
-| arc\_connected\_cluster\_id | The resource ID of the connected cluster to deploy Azure IoT Operations Platform to | `string` | n/a | yes |
-| cert\_manager | n/a | ```object({ version = string train = string })``` | n/a | yes |
-| connected\_cluster\_name | The name of the connected cluster to deploy Azure IoT Operations to | `string` | n/a | yes |
-| edge\_storage\_accelerator | n/a | ```object({ version = string train = string diskStorageClass = string faultToleranceEnabled = bool diskMountPoint = string })``` | n/a | yes |
-| enable\_instance\_secret\_sync | Whether to enable secret sync on the Azure IoT Operations instance | `bool` | 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 |
-| secret\_sync\_controller | n/a | ```object({ version = string train = string })``` | n/a | yes |
-| secret\_sync\_identity | Secret Sync Extension user managed identity id and client id | ```object({ id = string client_id = string })``` | n/a | yes |
-| trust\_config\_source | TrustConfig source must be one of 'SelfSigned', 'CustomerManagedByoIssuer' or 'CustomerManagedGenerateIssuer'. Defaults to SelfSigned. When choosing CustomerManagedGenerateIssuer, ensure connectedk8s proxy is enabled on the cluster for current user. When choosing CustomerManagedByoIssuer, ensure an Issuer and ConfigMap resources exist in the cluster. | `string` | n/a | yes |
+| Name | Description | Type | Default | Required |
+|----------------------------------|-------------------------------------------------------------------------------------|---------------------------------------------------|---------|:--------:|
+| aio\_namespace | Azure IoT Operations namespace | `string` | n/a | yes |
+| aio\_user\_managed\_identity\_id | ID of the User Assigned Managed Identity for the Azure IoT Operations instance | `string` | n/a | yes |
+| arc\_connected\_cluster\_id | The resource ID of the connected cluster to deploy Azure IoT Operations Platform to | `string` | n/a | yes |
+| connected\_cluster\_name | The name of the connected cluster to deploy Azure IoT Operations to | `string` | n/a | yes |
+| enable\_instance\_secret\_sync | Whether to enable secret sync on the Azure IoT Operations instance | `bool` | 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 |
+| secret\_sync\_controller | n/a | ```object({ version = string train = string })``` | n/a | yes |
+| secret\_sync\_identity | Secret Sync Extension user managed identity id and client id | ```object({ id = string client_id = string })``` | n/a | yes |
## Outputs
diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/main.tf b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/main.tf
index 9c876937..23421fcd 100644
--- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/main.tf
+++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/main.tf
@@ -5,22 +5,6 @@
*
*/
-locals {
- default_storage_class = var.edge_storage_accelerator.faultToleranceEnabled ? "acstor-arccontainerstorage-storage-pool" : "default,local-path"
- kubernetes_storage_class = var.edge_storage_accelerator.diskStorageClass != "" ? var.edge_storage_accelerator.diskStorageClass : local.default_storage_class
- diskMountPoint = coalesce(var.edge_storage_accelerator.diskMountPoint, "/mnt")
-
- container_storage_settings = var.edge_storage_accelerator.faultToleranceEnabled ? {
- "edgeStorageConfiguration.create" = "true"
- "feature.diskStorageClass" = local.kubernetes_storage_class
- "acstorConfiguration.create" = "true"
- "acstorConfiguration.properties.diskMountPoint" = local.diskMountPoint
- } : {
- "edgeStorageConfiguration.create" = "true"
- "feature.diskStorageClass" = local.kubernetes_storage_class
- }
-}
-
data "azapi_resource" "cluster_oidc_issuer" {
name = var.connected_cluster_name
parent_id = var.resource_group.id
@@ -42,37 +26,6 @@ resource "azurerm_arc_kubernetes_cluster_extension" "secret_store" {
"rotationPollIntervalInSeconds" = "120"
"validatingAdmissionPolicies.applyPolicies" = "false"
}
- depends_on = [azurerm_arc_kubernetes_cluster_extension.cert_manager]
-}
-
-resource "azurerm_arc_kubernetes_cluster_extension" "container_storage" {
- name = "azure-arc-containerstorage"
- cluster_id = var.arc_connected_cluster_id
- extension_type = "microsoft.arc.containerstorage"
- identity {
- type = "SystemAssigned"
- }
- version = var.edge_storage_accelerator.version
- release_train = var.edge_storage_accelerator.train
- configuration_settings = local.container_storage_settings
- depends_on = [azurerm_arc_kubernetes_cluster_extension.cert_manager]
-}
-
-resource "azurerm_arc_kubernetes_cluster_extension" "cert_manager" {
- count = var.trust_config_source != "CustomerManagedByoIssuer" ? 1 : 0
- name = "cert-manager"
- cluster_id = var.arc_connected_cluster_id
- extension_type = "microsoft.certmanagement"
- identity {
- type = "SystemAssigned"
- }
- version = var.cert_manager.version
- release_train = var.cert_manager.train
- release_namespace = "cert-manager"
- configuration_settings = {
- "AgentOperationTimeoutInMinutes" = var.aio_cert_manager_config.agent_operation_timeout_in_minutes
- "global.telemetry.enabled" = var.aio_cert_manager_config.global_telemetry_enabled
- }
}
resource "azurerm_federated_identity_credential" "federated_identity_cred_sse_aio" {
diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/variables.tf b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/variables.tf
index 7ef6acaa..e5c64c97 100644
--- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/variables.tf
+++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/variables.tf
@@ -3,26 +3,6 @@ variable "arc_connected_cluster_id" {
description = "The resource ID of the connected cluster to deploy Azure IoT Operations Platform to"
}
-variable "trust_config_source" {
- type = string
- description = "TrustConfig source must be one of 'SelfSigned', 'CustomerManagedByoIssuer' or 'CustomerManagedGenerateIssuer'. Defaults to SelfSigned. When choosing CustomerManagedGenerateIssuer, ensure connectedk8s proxy is enabled on the cluster for current user. When choosing CustomerManagedByoIssuer, ensure an Issuer and ConfigMap resources exist in the cluster."
-}
-
-variable "aio_cert_manager_config" {
- type = object({
- agent_operation_timeout_in_minutes = string
- global_telemetry_enabled = bool
- })
- description = "Install cert-manager and trust-manager extensions"
-}
-
-variable "cert_manager" {
- type = object({
- version = string
- train = string
- })
-}
-
variable "secret_sync_controller" {
type = object({
version = string
@@ -30,16 +10,6 @@ variable "secret_sync_controller" {
})
}
-variable "edge_storage_accelerator" {
- type = object({
- version = string
- train = string
- diskStorageClass = string
- faultToleranceEnabled = bool
- diskMountPoint = string
- })
-}
-
variable "resource_group" {
type = object({
id = string
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 7094dd69..c1ea5cc2 100644
--- a/src/100-edge/110-iot-ops/terraform/variables.init.tf
+++ b/src/100-edge/110-iot-ops/terraform/variables.init.tf
@@ -1,40 +1,12 @@
/*
* Optional Variables
*
- * IMPORTANT: The variable names in this file ('cert_manager', 'secret_sync_controller',
- * 'edge_storage_accelerator') are explicitly referenced by the
- * aio-version-checker.py script. If you rename these variables or change their structure,
+ * IMPORTANT: The variable names in this file ('secret_sync_controller')
+ * are explicitly referenced by the aio-version-checker.py script.
+ * If you rename these variables or change their structure,
* you must also update the script and the aio-version-checker-template.yml pipeline.
*/
-variable "cert_manager" {
- type = object({
- version = string
- train = string
- })
- default = {
- version = "0.7.0"
- train = "stable"
- }
-}
-
-variable "edge_storage_accelerator" {
- type = object({
- version = string
- train = string
- diskStorageClass = string
- faultToleranceEnabled = bool
- diskMountPoint = string
- })
- default = {
- version = "2.6.0"
- train = "stable"
- diskStorageClass = ""
- faultToleranceEnabled = false
- diskMountPoint = "/mnt"
- }
-}
-
variable "secret_sync_controller" {
type = object({
version = string
diff --git a/src/100-edge/110-iot-ops/terraform/variables.tf b/src/100-edge/110-iot-ops/terraform/variables.tf
index 2bc538b7..2ee7297f 100644
--- a/src/100-edge/110-iot-ops/terraform/variables.tf
+++ b/src/100-edge/110-iot-ops/terraform/variables.tf
@@ -53,18 +53,6 @@ variable "aio_ca" {
description = "CA certificate for the MQTT broker, can be either Root CA or Root CA with any number of Intermediate CAs. If not provided, a self-signed Root CA with a intermediate will be generated. Only valid when Trust Source is set to CustomerManaged"
}
-variable "aio_cert_manager_config" {
- type = object({
- agent_operation_timeout_in_minutes = string
- global_telemetry_enabled = bool
- })
- default = {
- agent_operation_timeout_in_minutes = "20"
- global_telemetry_enabled = true
- }
- description = "Install cert-manager"
-}
-
variable "enable_opc_ua_simulator" {
type = bool
default = true
diff --git a/src/100-edge/120-observability/bicep/README.md b/src/100-edge/120-observability/bicep/README.md
index 37e79b73..c209aec9 100644
--- a/src/100-edge/120-observability/bicep/README.md
+++ b/src/100-edge/120-observability/bicep/README.md
@@ -53,8 +53,8 @@ Creates the cluster extensions required to expose cluster and container metrics.
| Name | Type | API Version |
|:------------------------|:-----------------------------------------------|:------------|
-| azuremonitor-metrics | `Microsoft.KubernetesConfiguration/extensions` | 2023-05-01 |
-| azuremonitor-containers | `Microsoft.KubernetesConfiguration/extensions` | 2023-05-01 |
+| azuremonitor-metrics | `Microsoft.KubernetesConfiguration/extensions` | 2024-11-01 |
+| azuremonitor-containers | `Microsoft.KubernetesConfiguration/extensions` | 2024-11-01 |
#### Outputs for clusterExtensionsObs
diff --git a/src/100-edge/120-observability/bicep/modules/cluster-extensions-obs.bicep b/src/100-edge/120-observability/bicep/modules/cluster-extensions-obs.bicep
index e72d73a1..51ca5d40 100644
--- a/src/100-edge/120-observability/bicep/modules/cluster-extensions-obs.bicep
+++ b/src/100-edge/120-observability/bicep/modules/cluster-extensions-obs.bicep
@@ -48,7 +48,7 @@ resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09
scope: resourceGroup(logAnalyticsWorkspaceResourceGroupName)
}
-resource containerMetricsExtension 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' = {
+resource containerMetricsExtension 'Microsoft.KubernetesConfiguration/extensions@2024-11-01' = {
scope: arcConnectedCluster
name: 'azuremonitor-metrics'
properties: {
@@ -66,7 +66,7 @@ resource containerMetricsExtension 'Microsoft.KubernetesConfiguration/extensions
}
}
-resource containerLogsExtension 'Microsoft.KubernetesConfiguration/extensions@2023-05-01' = {
+resource containerLogsExtension 'Microsoft.KubernetesConfiguration/extensions@2024-11-01' = {
scope: arcConnectedCluster
name: 'azuremonitor-containers'
properties: {