Search code examples
terraform

Working around "known after apply" – how to check if a variable is unknown?


I'm trying to work around the "known after apply" issue I'm facing. To be exact, this is the code I'm dealing with:

resource "azurerm_security_center_storage_defender" "defender_for_storage" {
  for_each = {
    for id in [
      azurerm_storage_account.backups.id,
      azurerm_storage_account.media_blobs.id,
    ] : id => id
  }
  storage_account_id                          = each.value
  malware_scanning_on_upload_enabled          = true
  malware_scanning_on_upload_cap_gb_per_month = 10
  sensitive_data_discovery_enabled            = true
}

The error:

╷
│ Error: Invalid for_each argument
│ 
│   on storage.tf line 291, in resource "azurerm_security_center_storage_defender" "defender_for_storage":
│  291:   for_each = {
│  292:     for id in [
│  293:       azurerm_storage_account.backups.id,
│  294:       azurerm_storage_account.media_blobs.id,
│  295:     ] : id => id
│  296:   }
│     ├────────────────
│     │ azurerm_storage_account.backups.id is "***"
│     │ azurerm_storage_account.media_blobs.id is a string, known only after apply
│ 
│ The "for_each" map includes keys derived from resource attributes that
│ cannot be determined until apply, and so Terraform cannot determine the
│ full set of keys that will identify the instances of this resource.
│ 
│ When working with unknown values in for_each, it's better to define the map
│ keys statically in your configuration and place apply-time results only in
│ the map values.
│ 
│ Alternatively, you could use the -target planning option to first apply
│ only the resources that the for_each value depends on, and then apply a
│ second time to fully converge.
╵

The issue here is that the storage accounts might not exist yet, so in that case the "defender_for_storage" can't be created, but it would work if I remove the code, let Terraform create the storage accounts and then add the code back.

I'm aware there are other solutions to the specific example (like using -target or not using for_each) above but I'm specifically looking for a solution towards "known after apply" and creating resources based on those values.

My proposal would be to do something like this:

resource "azurerm_security_center_storage_defender" "defender_for_storage" {
  for_each = {
    for id in [
      isunknown(azurerm_storage_account.backups.id) ? "" : azurerm_storage_account.backups.id,
      isunknown(azurerm_storage_account.media_blobs.id) ? "" : azurerm_storage_account.media_blobs.id,
    ] : id => id if id != ""
  }
  storage_account_id                          = each.value
  malware_scanning_on_upload_enabled          = true
  malware_scanning_on_upload_cap_gb_per_month = 10
  sensitive_data_discovery_enabled            = true
}

In this case a special function isunknown in Terraform would just default to "" for the storage accounts that don't exist yet, filter out the unknown ones and thus proceed with the ones that exist. Then I could just run it again and it would create the remaining resources, without errors.

My goal is to avoid having to change the code every time I deploy this as there are multiple environments that use the same code and it would truly mess up the Git history.

Using -target makes the deployment less deterministic and prone to errors, and an additional workflow would have to be added, all of which adds additional work and uncertainty to the deployment process (in terms of errors and ability to move forward) which should ideally be just a click no matter what state is being used.


Solution

  • "Known-ness" is not exposed as something you can program with in Terraform, because that would undermine the promise that when you apply a plan Terraform will either do what the plan said or return an error explaining why it can't. (It would introduce the possibility of a value changing between the plan and apply phases, and Terraform's execution model relies on that not being possible.)

    The error message offered you two alternatives. You've already ruled out the -target option, but in your case I think the other option is more applicable: "define the map keys statically".

    For example:

      for_each = {
        backups     = azurerm_storage_account.backups.id
        media_blobs = azurerm_storage_account.media_blobs.id
      }
    

    This would declare two instances with known instance keys:

    • azurerm_security_center_storage_defender.defender_for_storage["backups"]
    • azurerm_security_center_storage_defender.defender_for_storage["media_blobs"]

    Each one would be configured with the corresponding storage account id.