Skip to content

Terraform recipe lists objects that it only reads with a data block as Radius-managed output resources #13418

Open

Description

@AzureMike

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/redisCaches and Radius.Security/secrets resource types are registered, as in radius-project/resource-types-contrib.
  • The dynamic-rp Pod can download providers from registry.terraform.io.
  • rad, kubectl, and a workspace that points to the cluster.
  • Placeholders: <namespace> is a new, empty namespace, for example tf-demo. <group> is a new Radius resource group, for example tf-demo.
  1. 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
  2. Save this Terraform module as mod/main.tf. It reads sentinel with a data block 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
    }
  3. 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
  4. Save bicepconfig.json:

    { "extensions": { "radius": "br:biceptypes.azurecr.io/radius:0.61" } }
  5. 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>'
          }
        }
      }
    }
  6. 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
      }
    }
  7. 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
  8. 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

  • rad CLI 0.61.1 (commit b913c13618039677b8727a2063cc853142cdb7c9), Bicep 0.46.1.
  • Radius control plane 0.61.1. Terraform v1.15.8 (linux_arm64) in dynamic-rp. The hashicorp/kubernetes provider is v2.38.0.
  • Kubernetes: AKS v1.35.6, Azure Linux 3.0, arm64 nodes.
  • Radius.Data/redisCaches and Radius.Security/secrets types and the secrets recipe from resource-types-contrib 0c3eaf14e7acc074420e78fda4d66c51541a8145.

Additional context

Source at v0.61.1:

  • getDeployedOutputResources loops over every entry of module.Resources and selects only by provider name. It does not check resource.Mode. The terraform-json StateResource type has a Mode field with the values data and managed. The same loop is on main at f7fd1ee.
  • GetOutputResourcesFromRecipe sets RadiusManaged: true for every ID that the driver returns.
  • The Terraform driver's Delete runs terraform destroy and does not use OutputResources. This matches the observed delete. The Bicep driver's Delete uses RadiusManaged to 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

  1. added
    bugSomething is broken or not working as expected
    on Oct 23, 2026
  2. radius-triage commented on Oct 23, 2026

    @radius-triage

    👋 @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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or not working as expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions