Repository navigation
chore(terraform): move cross-variable validations to lifecycle preconditions - #862
Conversation
…ditions Terraform v1.15 is stricter about variable validation blocks that reference other variables. Move the 10 cross-variable validations listed in microsoft#608 out of their variable blocks and into lifecycle preconditions on a new terraform_data "validate_inputs" resource in each affected component or module, keeping the conditions and error messages as they were: - 080-azureml: ml_workload_identity, network_security_group, nat_gateway - 100-cncf-cluster: both arc_onboarding_identity checks, key_vault, should_generate_cluster_server_token - 100-cncf-cluster/modules/ubuntu-k3s: should_generate_cluster_server_token - 036-managed-redis: private_endpoint_subnet - 031-fabric: fabric_capacity_admins In 036-managed-redis and 031-fabric the module that uses the checked value now depends on the guard, so a failed precondition is the only plan error. The 031-fabric and 100-cncf-cluster tests now expect the failure on terraform_data.validate_inputs instead of the variable, and the component READMEs are regenerated with terraform-docs. Fixes microsoft#608 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| depends_on = [terraform_data.validate_inputs] | ||
|
|
There was a problem hiding this comment.
Is this depends_on needed? A failing precondition on terraform_data.validate_inputs stops the plan regardless of graph ordering (custom conditions), and the 080-azureml, 100-cncf-cluster, and ubuntu-k3s guards don't add one. On a module, depends_on also defers any data source reads inside it, which HashiCorp recommends using only as a last resort. Could we drop it here and in 036-managed-redis/terraform/main.tf (L35) for consistency?
| depends_on = [terraform_data.validate_inputs] |
There was a problem hiding this comment.
Agreed, removed it here and in 036-managed-redis in 37f35b5. The 031-fabric tests still pass, including create_capacity_without_admins.
There was a problem hiding this comment.
Removed from both module calls in 37f35b5. One change in b0edf27: Redis now presence-gates should_enable_private_endpoint on private_endpoint_subnet (katriendg's follow-up). Gating alone would make a plan targeting module.managed_redis skip the endpoint without an error, so I put depends_on = [terraform_data.validate_inputs] back on that one call. The Redis module has no data sources, so nothing gets deferred. Fabric still has no depends_on. I've left this thread open so you can check that exception. Happy to drop it if you'd rather accept the targeted-plan gap.
📚 Documentation Health ReportGenerated on: 2026-10-07 21:36:02 UTC 📈 Documentation Statistics
🏗️ Three-Tree Architecture Status
🔍 Quality Metrics
This report is automatically generated by the Documentation Automation workflow. |
…ions - move remaining cross-variable validations in 100-cncf-cluster (should_use_script_from_secrets_for_deploy, should_deploy_over_ssh) and 080-azureml (compute_cluster_min_nodes <= max) into validate_inputs - gate NAT gateway, NSG and ML workload identity wiring in 080-azureml on the object being set so the precondition is the only plan error - drop depends_on on validate_inputs in 031-fabric and 036-managed-redis - trim guard comments and add Input Validation headers - add plan tests for the moved 100-cncf-cluster rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com>
Katrien De Graeve (katriendg)
left a comment
There was a problem hiding this comment.
Please address the first-upgrade dependency path introduced by the nested validation guard, which can defer the Arc cluster lookup and force RBAC replacement. Please also refresh the PR description and attach current-head Terraform 1.15+ validation and regression evidence.
Reconcile the PR description with the follow-up commit
The PR body still describes the first revision. Fabric and Redis module calls no longer depend on the guard. AzureML min/max and the three cluster script/SSH checks listed as deferred have now moved, bringing the guard total to fourteen preconditions. The min-node rule is split with narrower messages; this does not violate preservation of the original issue's ten messages. AzureML now uses try() and object-presence gates for NAT, NSG, workspace identity and inference identity, so the body should not describe their earlier double-error behavior as current without qualifying it. The test summary omits three added plan runs and the added-tests checklist remains unchecked. This misstates the reviewed deliverable, independently of the four already-addressed source comments.
Update Description, Implementation Details, Additional Notes and the test inventory for commit 37f35b5: ten original migrations plus four additional checks; no explicit guard depends_on on Fabric/Redis module calls; AzureML object-presence gates and try(); split min-node messages; three new cluster plan runs. Retain only genuinely deferred components/blueprints in follow-up scope. Keep runtime success claims explicitly author-reported and version/head/fixture-specific. Reconcile the added-tests and regression-test checklist entries with committed tests without marking execution or human-review boxes complete prematurely.
The terraform_data guards in 080-azureml, 100-cncf-cluster and ubuntu-k3s were new managed resources inside existing depends_on boundaries. On the first plan after upgrading, their create action deferred data source reads behind those boundaries (for example the Arc connected cluster lookup that feeds the Arc RBAC scope), which can force role assignment replacement. - 080-azureml, 100-cncf-cluster: move the preconditions onto the existing data.azurerm_client_config.current, which is always read and adds no planned change - ubuntu-k3s: drop the nested guard; check the token rule as a precondition on random_string.cluster_server_token instead - 080-azureml: module.network depends on the data source so targeted plans (workspace and inference integration inherit it) still run the checks instead of silently skipping the NAT/NSG association - point the cluster plan tests at the data source and regenerate READMEs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com>
|
Katrien De Graeve (@katriendg) thanks for the thorough review. I've updated the description for head 7b71003. It now covers the 10 original and 4 additional preconditions and where each one lives, the Fabric/Redis guards having no |
…#875) Adds plan-level Terraform tests for the last two untested components listed in #152: `080-azureml` and `140-azureml`. Each component gets a `tests/base.tftest.hcl` and a `tests/setup` module, following the pattern from #821 and #874. All runs use `command = plan` with `mock_provider`, so no Azure credentials are needed. ## Related Issue Relates to #152 With #821, #861 and #874, this covers the remaining components in that issue. It doesn't touch any component code. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Blueprint modification or addition - [ ] Component modification or addition - [ ] Documentation update - [ ] CI/CD pipeline change - [x] Other (please describe): tests only, no changes to component code ## Implementation Details `080-azureml` (mocks `azapi` and `azurerm`; `azurerm_client_config` is overridden): - `create_default_configuration`: the workspace name (`mlw-...`), location and resource group, the compute subnet name (`subnet-<prefix>-azureml-...`) and the compute cluster name (`cluster-...`). No registry, private endpoint, DNS zones, AKS integration or workload identity by default. - `create_with_custom_names_and_existing_subnet`: provided workspace and cluster names are used. With `should_create_compute_cluster_snet = false` and `compute_cluster_subnet`, no subnet is created but the cluster still is. - `create_without_compute_cluster`: neither the cluster nor its subnet is created, and the cluster outputs are null. - `create_with_private_endpoint`: the workspace private endpoint name (`pe-mlw-...`) and the two private DNS zones (`privatelink.api.azureml.ms`, `privatelink.notebooks.azure.net`). - `integrate_aks_cluster`: the extension name, and the compute target name with hyphens removed and truncated to 16 characters (`environment = "staging-west"`). - `reject_invalid_inputs` / `reject_invalid_resource_prefix`: the single-variable validations (`compute_cluster_max_nodes`, `compute_cluster_vm_priority`, `inference_router_service_type`, `aks_cluster_purpose`, `resource_prefix`). `140-azureml` (mocks `azapi`, `azurerm` and `tls`): - `create_default_configuration`: extension name (`azureml-...`) and compute target name (`arck-...`), the target's workspace, location, `Kubernetes` compute type and connected cluster ID, the default description, local auth disabled, `azureml` namespace, the component's default instance type (`8Gi` memory limit), `Stable` release train, and a system-assigned identity. - `create_with_custom_compute_settings`: provided names, description, local auth, namespace, default instance type, release train and virtual cluster name are passed through. Provided instance types replace the default, and `cluster_integration_workspace_identity_id` switches the target to a user-assigned identity. - `configure_extension_with_tls_tolerations_and_workload_identity`: runs the `inference-cluster-integration` module directly, because the extension settings and federated credentials are not exposed as component outputs. Checks the string-typed feature flags, `relayserver.enabled = "true"`, `allowInsecureConnections = "false"` with the protected TLS settings when CNAME, certificate and key are all set, the indexed `toleration[n]` / `workLoadToleration[n]` keys (unset fields left out), and one federated identity credential per subject, with the name derived from the subject, the cluster OIDC issuer and the workload identity. - `reject_invalid_inference_router_service_type`: the variable validation. Some outputs (`kubernetes_compute`, `azureml_extension`) wrap whole resources in `try()`, so a single unknown computed attribute makes them unknown at plan. The tests override only the computed attributes they depend on (the extension `id` and identity principal, and the compute `id`) with `override_during = plan`. The 080 tests don't cover the cross-variable validations (`nat_gateway`, `network_security_group`, `ml_workload_identity`, min/max nodes) because #862 moves them. I also ran these 080 tests against the #862 head, and all 8 runs pass there too. ## Testing Performed - [ ] Terraform plan/apply - [ ] Blueprint deployment test - [ ] Unit tests - [ ] Integration tests - [ ] Bug fix includes regression test (see [Test Policy](docs/contributing/testing-validation.md)) - [ ] Manual validation - [x] Other: `terraform test` (plan-level, mock providers) Run with no `ARM_*` or `AZURE_*` variables set: - `terraform init -backend=false` and `terraform test` in each component, with Terraform 1.16.5 and 1.15.0: `080-azureml` 8 passed, 0 failed; `140-azureml` 5 passed, 0 failed. - `terraform validate` in each component: valid. - `terraform fmt -check -recursive` on both `tests/` directories: no changes. - `tflint --disable-rule=terraform_required_providers --recursive` with the repo `.tflint.hcl`: no findings. I also checked that the tests fail when the component is wrong. I broke a copy of each component, one change at a time, and every one of these 10 changes made at least one run fail: - the subnet label - the cluster name prefix - a DNS zone name - the 16-character truncation - creating the subnet without a cluster - `relayserver.enabled` - the TLS check - the default instance type memory - the `workLoadToleration` key - the user-assigned identity block ## Additional Notes While writing the registry case for 080 I found that `should_deploy_registry = true` currently fails at plan with azapi 2.13.0: ``` embedded schema validation failed: the argument "body" is invalid: `properties.description` is not expected here. ``` The root module always passes a `description` (`coalesce(var.registry_description, ...)`), and the `Microsoft.MachineLearningServices/registries@2025-07-01-preview` schema has no `properties.description`, so the registry can't be planned with schema validation on. I left the registry out of these tests. I'm happy to open a separate issue or fix for it if you'd like. ## Validation Steps 1. `cd src/000-cloud/080-azureml/terraform && terraform init -backend=false && terraform test` 2. `cd src/100-edge/140-azureml/terraform && terraform init -backend=false && terraform test` No Azure login is needed for either. ## Checklist - [ ] I have updated the documentation accordingly - [x] I have added tests to cover my changes - [ ] All new and existing tests passed - [x] I have run `terraform fmt` on all Terraform code - [x] I have run `terraform validate` on all Terraform code - [ ] I have run `az bicep format` on all Bicep code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Katrien De Graeve <katriendg@users.noreply.github.com>
Katrien De Graeve (katriendg)
left a comment
There was a problem hiding this comment.
Reviewed 7b71003 locally on Terraform 1.15.0 and 1.16.5: 37 test runs passed on each, including the committed real-provider CNCF suite and additional isolated AzureML full/targeted negative cases. The original Fabric initialization failure reproduces on main. Focused validation, formatting and TFLint passed.
The earlier guard-induced replacement and targeted-plan concerns are addressed. An Azure-backed comparison using the existing cluster state and equivalent inputs has identical actions and replacement paths on PR and main. Both still propose 35 replacements from existing drift, so neither plan should be applied without separate investigation. Existing-state AzureML/Redis/Fabric upgrades were not covered.
One non-blocking cleanup: Redis with an enabled private endpoint and missing subnet reports the intended guard error plus a secondary missing subnet_id error. Consider the same presence-gating approach used for AzureML. Please retain the AzureML negative regression cases in the suite now available on main, and reconcile/close the six unresolved threads.
📚 Documentation Health ReportGenerated on: 2026-10-14 12:13:07 UTC 📈 Documentation Statistics
🏗️ Three-Tree Architecture Status
🔍 Quality Metrics
This report is automatically generated by the Documentation Automation workflow. |
…ML guard tests - 036-managed-redis: pass should_enable_private_endpoint to the module only when private_endpoint_subnet is set, so a missing subnet reports just the precondition instead of also failing on the private endpoint's subnet_id. The module call depends on the guard so a plan targeting module.managed_redis still runs the check instead of silently skipping the endpoint; the module has no data sources, so no reads are deferred. - 080-azureml: add the cross-variable negative cases to the plan-level suite now on main: NAT gateway (full and module.network targeted), NSG (targeted), workload identity roles (full and module.workspace targeted), and min nodes above max nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Shivam Sharma <192242476+ShivamSharma43@users.noreply.github.com>
|
Katrien De Graeve (@katriendg) thanks for the review and the approval. Follow-up commit b0edf27:
Checks on Terraform 1.16.5 and 1.15.0: the 080-azureml suite passed 14/14. |
Pull Request
Description
Moves the 10 cross-variable
validationblocks listed in #608 out of theirvariableblocks and intolifecycle.preconditionblocks, and moves 4 more cross-variable checks that were added to080-azuremland100-cncf-clusterafter the issue was filed. That is 14 preconditions in total. The original 10 keep their conditions and error messages.With Terraform 1.15.0,
terraform testfails before any run starts in components where a test references one of these variables:I reproduced this on
mainwith Terraform 1.15.0 in031-fabricand in100-cncf-cluster(var.should_assign_roles).Where the preconditions live. The issue suggests a
terraform_dataguard resource. I kept that in031-fabricand036-managed-redis. In080-azureml,100-cncf-clusterandubuntu-k3sa new managed resource sits inside existingdepends_onchains: its one-time create on the first plan after upgrading defers data source reads further down those chains (for exampledata.azapi_resource.arc_connected_cluster, which sets the Arc RBAC role assignmentscope), which can replace existing role assignments. So in those three places the preconditions are on resources that already exist and add no planned change (see Implementation Details).Related Issue
Fixes #608
Type of Change
Implementation Details
src/000-cloud/031-fabricterraform_data.validate_inputsfabric_capacity_adminssrc/000-cloud/036-managed-redisterraform_data.validate_inputsprivate_endpoint_subnetsrc/000-cloud/080-azuremldata.azurerm_client_config.currentml_workload_identity,network_security_group,nat_gatewaycompute_cluster_min_nodes <= compute_cluster_max_nodessrc/100-edge/100-cncf-clusterdata.azurerm_client_config.currentarc_onboarding_identity(both checks),key_vault,should_generate_cluster_server_tokenshould_use_script_from_secrets_for_deploy,should_deploy_over_ssh(two checks)src/100-edge/100-cncf-cluster/modules/ubuntu-k3srandom_string.cluster_server_tokenshould_generate_cluster_server_tokenazurerm_client_config.currenthas nocountand is already read in both components.ubuntu-k3s. The nested guard is gone. The rule now sits onrandom_string.cluster_server_token, which only exists whenshould_generate_cluster_server_token = true, so the condition there is justvar.cluster_server_token == null. When the module is called from100-cncf-cluster, a conflicting pair still reports the message twice (root and module).compute_cluster_min_nodes. The rule is split:>= 0stays as a variable validation (it only references itself), and<= compute_cluster_max_nodesis a precondition with its own message.080-azuremlnull handling. The module inputs are null-safe so a failed check does not also produceAttempt to get attribute from null value/Missing required argumenterrors:should_enable_nat_gateway,should_associate_network_security_groupand the twoml_workload_identityflags are gated on the object being present, andnat_gateway_id = try(var.nat_gateway.id, null).module.networkhasdepends_on = [data.azurerm_client_config.current]so the checks also run in targeted plans (module.workspaceandmodule.inference_cluster_integrationinherit it through their existingdepends_on). Without it,-target='module.network[0]'skipped the checks and the presence gate quietly dropped the NAT/NSG association.module.networkhas no data sources, and the data source can only be deferred when the whole component is, so this does not add deferred reads.depends_onon the Fabric/Redis guards.module.fabric_capacityandmodule.managed_redisdo not depend on the guard.031-fabric/tests/base.tftest.hcl(create_capacity_without_admins) expects the failure onterraform_data.validate_inputs. In100-cncf-cluster/tests/cluster.tftest.hcl,test_invalid_multiple_identitiesexpects it ondata.azurerm_client_config.current, and three new plan runs cover the additional checks:test_invalid_script_from_secrets_without_key_vault_upload,test_invalid_ssh_without_arc_machinesandtest_invalid_ssh_without_credentials.markdown-table-formatter(as innpm run tf-docs).031-fabricand036-managed-redisgain theterraform_data.validate_inputsrow; the080-azureml,100-cncf-clusterandubuntu-k3sREADMEs end up with no resource changes.Testing Performed
terraform test(plan-level, mocked providers),terraform validate,terraform fmt, TFLintAll results below are my local runs on head
7b710035, with Terraform 1.16.5 and 1.15.0 on Windows, and noARM_*/AZURE_*variables set. Nothing was planned or applied against Azure.terraform init -backend=false+terraform validatein031-fabric,036-managed-redis,080-azuremland100-cncf-cluster: valid.terraform fmt -check -recursive: no changes..tflint.hcl,--disable-rule=terraform_required_providers --recursive) in080-azuremland100-cncf-cluster: no findings.031-fabric,terraform test: 5 passed, 0 failed on both versions. Onmainwith 1.15.0 it fails with the error above.100-cncf-cluster: the committedcluster.tftest.hcluses the realazurermprovider and needs an Azure login, which I don't have. In a local copy withmock_providerblocks forazurerm,azureadandazapi(not committed), all 13 committed runs pass on both versions. Extra temporary runs in that copy:cluster_server_token: plans.should_upload_to_key_vault = truewithkey_vault = null: fails ondata.azurerm_client_config.currentonly.cluster_server_tokenset together withshould_generate_cluster_server_token = true: fails with the original message from the root data source and frommodule.ubuntu_k3s.random_string.cluster_server_token.should_assign_roles = truewith no identity: fails with the original message, plusMissing required argumentfrommodule.role_assignments(see Additional Notes).080-azuremlhas no committed tests. A temporary mocked test file (not committed) passes 8/8 on both versions: valid full plan and validmodule.networktargeted plan with a NAT gateway;nat_gatewaymissing in a full plan and in amodule.networktargeted plan;network_security_groupmissing in amodule.networktargeted plan;ml_workload_identitymissing in a full plan and in amodule.workspacetargeted plan; min nodes greater than max nodes. Each failing case reports only the precondition. The same targeted NAT case against the previous head (37f35b5b) planned without any error.terraform_dataguard inside a module that another moduledepends_on, with a data source thatdepends_onthe second module, made the next plan read the data source during apply and replace a resource keyed on its result. Putting the same precondition on an existing data source instead gaveNo changeswith valid inputs, and still failed at plan with invalid inputs, including when that data source's own read was deferred.Validation Steps
cd src/000-cloud/031-fabric/terraform && terraform init -backend=false && terraform testwith Terraform 1.15.x: passes on this branch, fails onmainwithReference to uninitialized variable.terraform init -backend=false && terraform validatein each of the four component directories.080-azuremlwithshould_enable_nat_gateway = trueand nonat_gateway, with and without-target='module.network[0]'. Both should fail withnat_gateway must be provided when should_enable_nat_gateway is true.Checklist
terraform fmton all Terraform codeterraform validateon all Terraform codeaz bicep formaton all Bicep codeaz bicep buildto validate all Bicep code"All new and existing tests passed" is unchecked because the committed
100-cncf-clustertests have only been run in the mocked copy described above, not with a real Azure login.Security Review
Additional Notes
100-cncf-cluster. A variable validation stopped evaluation before anything used the bad value; a precondition does not. Withshould_assign_roles = trueand no identity, the precondition error is followed byMissing required argumenterrors frommodule.role_assignments. The plan still fails with the original message. I left this as is to keep the change focused. Makingmodule.role_assignmentsdepend on the data source would reduce it to a single error, and I'm happy to add that if you prefer.080-azuremland100-cncf-clusterno longer add any managed resource, so blueprint modules thatdepends_onthem (for exampleedge_azuremlwithdepends_on = [module.cloud_azureml]infull-multi-node-clusterandrobotics) get no extra deferred reads from this change. No blueprintdepends_onthecloud_fabricorcloud_managed_redismodule calls.mainnot changed here:010-security-identityand030-data(network_security_perimeter_id),055-vpn-gateway(existing_certificate_name),085-ai-foundry(private_endpoint_subnet_id),110-iot-ops(byo_issuer_trust_settings,aio_ca), and in blueprintscompute_cluster_min_nodes(azureml,robotics) andcluster_server_ip(full-multi-node-cluster). As far as I can tell, 1.15.0 only fails when a test references such a variable, and no test references these today. I can move them in a follow-up.terraformVersion: latestpin mentioned at the end of the issue.Overlap with #861: that PR adds a
036-managed-redistest (create_private_endpoint_without_subnet) that expectsvar.private_endpoint_subnet. Whichever of the two merges second needs thatexpect_failureschanged toterraform_data.validate_inputs; I'll update whichever is still open.🤖 Generated with Claude Code