Best practices for handling "known after apply" plan verbosity in TPF resources

Problem statement - TPF increases plan output verbosity

When migrating resources from SDK v2 over to TPF we have found that all computed attributes not defined in the resource configuration generate a -> (known after apply) entry.

Example SDK v2 implementation plan output modifying single attribute:

  # mongodbatlas_advanced_cluster.test6 will be updated in-place
  ~ resource "mongodbatlas_advanced_cluster" "test6" {
        id                                   = "Y2x1c3Rlcl9pZA==:Njc4Nzk5ZDI3MWI0ZmQyNjJiMGQ2YjA2-Y2x1c3Rlcl9uYW1l:T3RoZXJOYW1lNg==-cHJvamVjdF9pZA==:NjUwOTcyODQ4MjY5MTg1YzU1ZjQwY2Ex"
        name                                 = "OtherName6"
        # (18 unchanged attributes hidden)

      + pinned_fcv {
          + expiration_date = "2025-01-15T11:00:44Z"
        }

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Example TPF implementation plan output modifying single attribute:

# mongodbatlas_advanced_cluster.test will be updated in-place
  ~ resource "mongodbatlas_advanced_cluster" "test" {
      ~ advanced_configuration               = {
          ~ change_stream_options_pre_and_post_images_expire_after_seconds = -1 -> (known after apply)
          ~ default_read_concern                                           = "" -> (known after apply)
          ~ default_write_concern                                          = "" -> (known after apply)
          ~ fail_index_key_too_long                                        = false -> (known after apply)
          ~ javascript_enabled                                             = true -> (known after apply)
          ~ minimum_enabled_tls_protocol                                   = "TLS1_2" -> (known after apply)
          ~ no_table_scan                                                  = false -> (known after apply)
          ~ oplog_min_retention_hours                                      = 0 -> (known after apply)
          ~ oplog_size_mb                                                  = 0 -> (known after apply)
          ~ sample_refresh_interval_bi_connector                           = 0 -> (known after apply)
          ~ sample_size_bi_connector                                       = 0 -> (known after apply)
          ~ transaction_lifetime_limit_seconds                             = 0 -> (known after apply)
        } -> (known after apply)
      ~ bi_connector_config                  = {
          ~ enabled         = false -> (known after apply)
          ~ read_preference = "secondary" -> (known after apply)
        } -> (known after apply)
      ~ cluster_id                           = "67877b94cad4d051941efa6e" -> (known after apply)
      + config_server_management_mode        = (known after apply)
      + config_server_type                   = (known after apply)
      ~ connection_strings                   = {
          ~ private          = "" -> (known after apply)
          + private_endpoint = (known after apply)
          ~ private_srv      = "" -> (known after apply)
          ~ standard         = "mongodb://othername-shard-00-00.li87i.mongodb-qa.net:27017,othername-shard-00-01.li87i.mongodb-qa.net:27017,othername-shard-00-02.li87i.mongodb-qa.net:27017/?ssl=true&authSource=admin&replicaSet=atlas-nidyp1-shard-0" -> (known after apply)
          ~ standard_srv     = "mongodb+srv://othername.li87i.mongodb-qa.net" -> (known after apply)
        } -> (known after apply)
      ~ encryption_at_rest_provider          = "NONE" -> (known after apply)
      ~ global_cluster_self_managed_sharding = false -> (known after apply)
      ~ mongo_db_version                     = "8.0.4" -> (known after apply)
        name                                 = "OtherName"
      ~ paused                               = false -> (known after apply)
      + pinned_fcv                           = {
          + expiration_date = "2025-01-15T09:40:44Z"
          + version         = (known after apply)
        }
      ~ replication_specs                    = [
          ~ {
              ~ container_id   = {
                  - "AWS:US_EAST_1" = "67877b94cad4d051941efa6b"
                } -> (known after apply)
              ~ external_id    = "67877b94cad4d051941efa56" -> (known after apply)
              ~ id             = "67877b94cad4d051941efa55" -> (known after apply)
              ~ region_configs = [
                  ~ {
                      + analytics_auto_scaling = (known after apply)
                      ~ analytics_specs        = {
                          ~ disk_iops       = 3000 -> (known after apply)
                          ~ disk_size_gb    = 10 -> (known after apply)
                          ~ ebs_volume_type = "STANDARD" -> (known after apply)
                          ~ instance_size   = "M10" -> (known after apply)
                          ~ node_count      = 0 -> (known after apply)
                        } -> (known after apply)
                      ~ auto_scaling           = {
                          ~ compute_enabled            = false -> (known after apply)
                          ~ compute_max_instance_size  = "" -> (known after apply)
                          ~ compute_min_instance_size  = "" -> (known after apply)
                          ~ compute_scale_down_enabled = false -> (known after apply)
                          ~ disk_gb_enabled            = false -> (known after apply)
                        } -> (known after apply)
                      ~ electable_specs        = {
                          ~ disk_iops       = 3000 -> (known after apply)
                          ~ disk_size_gb    = 10 -> (known after apply)
                          ~ ebs_volume_type = "STANDARD" -> (known after apply)
                            # (2 unchanged attributes hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
              ~ zone_id        = "67877b94cad4d051941efa54" -> (known after apply)
              ~ zone_name      = "ZoneName managed by Terraform" -> (known after apply)
                # (1 unchanged attribute hidden)
            },
        ]
      ....
        # (4 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Available mitigations

UseStateForUnknown plan modifier is provided as a way of reducing these entries. This however has the following limitations:

  • Modifying unknown values to known values using the state can only be done when the value of the attribute will not change. This has the risk of encountering produced inconsistent result after apply if used incorrectly.
  • Defining a value as known can have side effects on update operation logic which relies on the plan. Update logic will be coupled with any plan modifier logic for reducing plan verbosity.

Open questions

Is there any alternative recommendation for reducing this plan verbosity in plan outputs?
Some options which come to mind:

  • Resource-level configuration to preserve SDK v2 behaviour of not displaying -> (known after apply).
  • Configurable flag from user side when calling plan/apply operation to not display → (known after apply).
  • A way to differentiate when ModifyPlan is called before showing the plan to the user and when called just before update operation (more aggressive / without unknowns to show to the user, less aggressive / with more unknowns for Update)

The framework provider’s output seems more correct to me: These are values which will be computed during apply, so it’s pretty bold of the SDK’s plan to assert that those attributes are unchanged, when we don’t really know what’s going to happen with them.

I wonder if the SDKv2 implementation is producing warnings about data consistency rule violations when these “unchanged” values actually do change? You can check by setting TF_LOG=WARN. Search this document for certain resource schema definition and data consistency errors for additional context.

As I understand it, the framework is more strict about this kind of thing than the SDKv2, so you may be stuck with the extra plan output if you’re not confident that the values are constant (can’t use UseStateForUnknown)

Hi @AgustinBettati,

There are probably multiple details involved here giving you a mismatch between the SDK and framework, but a big piece of the puzzle is probably what @hQnVyLRx was alluding to, Terraform allowed the SDK to get away with certain data inconsistencies for compatibility reasons.

As you are seeing, the framework assumes a default behavior of all computed values becoming unknown if there’s a change during the plan rather than assume they will remain constant. Which way to implement that was really a toss up, but assuming computed values will be unknown was the more conservative approach that avoids more errors in the long run.

The UseStateForUnknown plan modifier is the solution if you want to have those values remain constant during a plan. The “limitations” you have listed there are not really limitations of the modifiers, they are just explaining the data lifecycle rules. You can’t choose yourself what Terraform shows to the user, you can only modify what your provider is telling Terraform it intends to do; so don’t think of this as changing the verbosity, it’s changing the actual plan and will have downstream affects on other resources using the computed values.