Repository navigation
Terraform recipe lists objects that it only reads with a data block as Radius-managed output resources #13418
Description
A Terraform recipe often reads an existing object with a data block, for example a shared ConfigMap, a resource group, or a network. When the recipe runs, Radius adds that object to the resource's status.outputResources with radiusManaged: true. rad app graph also shows it as a resource of the recipe resource. The recipe does not create or own that object. In this test, rad app delete did not delete the object, because deletion of a Terraform recipe runs terraform destroy. But the resource status and the application graph show ownership of an object that another team or application can own.
Steps to reproduce
Prerequisites:
- Radius 0.61.1 on a Kubernetes cluster. The
Radius.Data/redisCachesandRadius.Security/secretsresource types are registered, as inradius-project/resource-types-contrib. - The
dynamic-rpPod can download providers fromregistry.terraform.io. rad,kubectl, and a workspace that points to the cluster.- Placeholders:
<namespace>is a new, empty namespace, for exampletf-demo.<group>is a new Radius resource group, for exampletf-demo.
-
Create the namespace and an object that the recipe only reads:
kubectl create namespace <namespace> kubectl -n <namespace> create configmap sentinel --from-literal=k=v
-
Save this Terraform module as
mod/main.tf. It readssentinelwith adatablock and creates one ConfigMap:terraform { required_providers { kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.36" } } } variable "context" { type = any } locals { namespace = var.context.runtime.kubernetes.namespace } # Reads an object that this recipe does not own. data "kubernetes_config_map" "sentinel" { metadata { name = "sentinel" namespace = local.namespace } } # The only object that this recipe creates. resource "kubernetes_config_map" "owned" { metadata { name = "owned-${var.context.resource.name}" namespace = local.namespace } data = { copied = data.kubernetes_config_map.sentinel.data["k"] } } output "result" { value = { values = { host = "example.invalid" port = 6379 } secrets = { url = "redis://example.invalid:6379" } } sensitive = true }
-
Serve the module as a zip archive inside the cluster. Any Terraform module source works; this step only avoids a public repository.
python3 -c "import zipfile; z=zipfile.ZipFile('mod.zip','w'); z.write('mod/main.tf','main.tf'); z.close()" kubectl -n <namespace> create configmap tfmod --from-file=mod.zip=mod.zip kubectl -n <namespace> apply -f - <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: tfmod spec: replicas: 1 selector: matchLabels: { app: tfmod } template: metadata: labels: { app: tfmod } spec: containers: - name: nginx image: nginx:1.27-alpine ports: [{ containerPort: 80 }] volumeMounts: [{ name: mod, mountPath: /usr/share/nginx/html, readOnly: true }] volumes: - name: mod configMap: { name: tfmod } --- apiVersion: v1 kind: Service metadata: name: tfmod spec: selector: { app: tfmod } ports: [{ port: 80, targetPort: 80 }] EOF kubectl -n <namespace> rollout status deploy/tfmod
-
Save
bicepconfig.json:{ "extensions": { "radius": "br:biceptypes.azurecr.io/radius:0.61" } } -
Save
pack.bicep. Replace<namespace>in both places:extension radius resource pack 'Radius.Core/recipePacks@2025-08-01-preview' = { name: 'tf-datasource' properties: { recipes: { 'Radius.Security/secrets': { kind: 'bicep' source: 'ghcr.io/radius-project/kube-recipes/secrets:latest' } 'Radius.Data/redisCaches': { kind: 'terraform' source: 'http://tfmod.<namespace>.svc.cluster.local/mod.zip' } } } } resource env 'Radius.Core/environments@2025-08-01-preview' = { name: 'tf' properties: { recipePacks: [ pack.id ] providers: { kubernetes: { namespace: '<namespace>' } } } }
-
Save
app.bicep:extension radius param environment string resource app 'Radius.Core/applications@2025-08-01-preview' = { name: 'tfprobe' properties: { environment: environment } } resource cache 'Radius.Data/redisCaches@2025-08-01-preview' = { name: 'cache' properties: { environment: environment application: app.id } }
-
Deploy and inspect:
rad group create <group> rad deploy pack.bicep -g <group> ENVID=/planes/radius/local/resourceGroups/<group>/providers/Radius.Core/environments/tf rad deploy app.bicep -g <group> -e $ENVID -p environment=$ENVID rad resource show Radius.Data/redisCaches cache -g <group> -o json | jq '.properties.status.outputResources' rad app graph tfprobe -g <group> --preview
-
Delete the application and check the object that the recipe only read:
rad app delete tfprobe -g <group> --preview -y kubectl -n <namespace> get configmap sentinel
Observed behavior
Step 7 lists sentinel as a Radius-managed output resource:
[
{
"id": "/planes/kubernetes/local/namespaces/<namespace>/providers/core/configmap/sentinel",
"localID": "",
"radiusManaged": true
},
{
"id": "/planes/kubernetes/local/namespaces/<namespace>/providers/core/configmap/owned-cache",
"localID": "",
"radiusManaged": true
}
]Displaying application: tfprobe
Name: cache (Radius.Data/redisCaches)
Connections: (none)
Resources:
owned-cache (core/configmap)
sentinel (core/configmap)
In step 8, rad app delete succeeds. The dynamic-rp log shows that terraform destroy only reads the data source and destroys only owned:
Running Terraform destroy with state lock timeout: 10m
module.default.data.kubernetes_config_map.sentinel: Reading...
module.default.data.kubernetes_config_map.sentinel: Read complete after 0s [id=<namespace>/sentinel]
# module.default.kubernetes_config_map.owned will be destroyed
Plan: 0 to add, 0 to change, 1 to destroy.
Destroy complete! Resources: 1 destroyed.
sentinel still exists after the delete. The behavior occurred in 2 of 2 deployments on 2026-10-22 (UTC), at 01:31Z and 01:33Z.
Desired behavior
Radius should record only objects that the recipe manages. In Terraform state, these are resources with "mode": "managed". Objects that are read with a data block ("mode": "data") should not be in outputResources, or should have radiusManaged: false. rad app graph should not show them as resources of the recipe resource.
Workaround
None verified.
System information
radCLI 0.61.1 (commitb913c13618039677b8727a2063cc853142cdb7c9), Bicep 0.46.1.- Radius control plane 0.61.1. Terraform v1.15.8 (linux_arm64) in
dynamic-rp. Thehashicorp/kubernetesprovider is v2.38.0. - Kubernetes: AKS v1.35.6, Azure Linux 3.0, arm64 nodes.
Radius.Data/redisCachesandRadius.Security/secretstypes and thesecretsrecipe from resource-types-contrib0c3eaf14e7acc074420e78fda4d66c51541a8145.
Additional context
Source at v0.61.1:
getDeployedOutputResourcesloops over every entry ofmodule.Resourcesand selects only by provider name. It does not checkresource.Mode. The terraform-jsonStateResourcetype has aModefield with the valuesdataandmanaged. The same loop is on main atf7fd1ee.GetOutputResourcesFromRecipesetsRadiusManaged: truefor every ID that the driver returns.- The Terraform driver's
Deleterunsterraform destroyand does not useOutputResources. This matches the observed delete. The Bicep driver'sDeleteusesRadiusManagedto decide which IDs to delete, so other code relies on this flag.
From the source, Azure data sources (with an id attribute) and AWS data sources (with an arn attribute) take the same path. I tested only the Kubernetes provider.
I found no existing issue. Searches in both repositories: "terraform data source", "data sources output resources", "terraform radiusManaged", "terraform resource mode".
Activity
radius-triage commentedon Oct 23, 2026
👋 @AzureMike Thanks for filing this bug report.
A project maintainer will review this report and get back to you soon. If you'd like immediate help troubleshooting, please visit our Discord server.
For more information on our triage process please visit our triage overview