Problem with null values in tfvars, works fine in a .tf file

I’m doing a refactor of our terraform. In the existing code, this works fine (excerpt). It creates an approle with no bound cidrs.

inputs.tf:

standard_apps = [
{
"app_name" : "test_app"
"auth" : [{
"auth_path" : "approle_auth"
"valid_boundaries" : ["prod", "dev"]
}]
"create_shared" : true
"engine_types" : ["kv-v2"]

},

approle_auth_entities.tf

locals {
approle_auth_roles = {
for reader in local.readers :
reader.app_boundary => reader if reader.auth_path == "approle_auth"
}
secret_auth_roles = {
for reader in local.readers :
reader.app_boundary => reader if reader.auth_path == "approle_auth" && reader.bind_secret_id != false
}
}

resource "vault_approle_auth_backend_role" "reader" {
for_each = local.approle_auth_roles

provider              = vault.admin
backend               = var.infra.approle_auth.path
role_name             = each.key
role_id               = each.key //Rather than use a GUID, we set the ID to be human-readable
bind_secret_id        = each.value.bind_secret_id
token_bound_cidrs     = can(each.value.bound_cidrs) ? each.value.bound_cidrs : null
secret_id_bound_cidrs = can(each.value.bound_cidrs) ? each.value.bound_cidrs : null
}

In the refactor, this is now in a tfvars (no change in content or code otherwise), and the behavior is different and it fails.

Terraform will perform the following actions:

  # module.standard_customers.vault_approle_auth_backend_role.reader["test_app_dev"] will be updated in-place
  ~ resource "vault_approle_auth_backend_role" "reader" {
        id                      = "auth/approle_auth/role/test_app_dev"
      ~ secret_id_bound_cidrs   = [
          + null,
        ]
      ~ token_bound_cidrs       = [
          + null,
        ]
        # (15 unchanged attributes hidden)
    }

  # module.standard_customers.vault_approle_auth_backend_role.reader["test_app_prod"] will be updated in-place
  ~ resource "vault_approle_auth_backend_role" "reader" {
        id                      = "auth/approle_auth/role/test_app_prod"
      ~ secret_id_bound_cidrs   = [
          + null,
        ]
      ~ token_bound_cidrs       = [
          + null,
        ]
        # (15 unchanged attributes hidden)
    }
	
	ā•·
│ Error: error updating AppRole auth backend role "auth/approle_auth/role/test_app_dev": Error making API request.
│
│ Namespace: admin
│ URL: PUT https://company-hcp-sre-dev-private-vault-dff8581e.86e0ac9c.z1.hashicorp.cloud:8200/v1/auth/approle_auth/role/test_app_dev
│ Code: 400. Errors:
│
│ * error parsing address "": Unable to convert "" to an IPv4 or IPv6 address, or a UNIX Socket
│
│   with module.standard_customers.vault_approle_auth_backend_role.reader["test_app_dev"],
│   on ..\..\..\..\modules\customers\standard\approle_auth_entities.tf line 14, in resource "vault_approle_auth_backend_role" "reader":
│   14: resource "vault_approle_auth_backend_role" "reader" {
│
╵
ā•·
│ Error: error updating AppRole auth backend role "auth/approle_auth/role/test_app_prod": Error making API request.
│
│ Namespace: admin
│ URL: PUT https://company-hcp-sre-dev-private-vault-dff8581e.86e0ac9c.z1.hashicorp.cloud:8200/v1/auth/approle_auth/role/test_app_prod
│ Code: 400. Errors:
│
│ * error parsing address "": Unable to convert "" to an IPv4 or IPv6 address, or a UNIX Socket
│
│   with module.standard_customers.vault_approle_auth_backend_role.reader["test_app_prod"],
│   on ..\..\..\..\modules\customers\standard\approle_auth_entities.tf line 14, in resource "vault_approle_auth_backend_role" "reader":
│   14: resource "vault_approle_auth_backend_role" "reader" {
│

aha. The fix was in my variable definition (plus the fact that it works when everything is in TF files, but not in tfvars, for some reason.)

Originally, I had this, but changing the default to []instead of a list with an empty string fixed it. When reading that as tfvars vs inside a .tf file, it must have done some unexpected casting in one of those scenarios.

bound_cidrs = optional(list(string), [ā€œā€])

variable "standard_apps" {
  description = "defines a standard customer"
  type = list(object({
    app_name = string // This will create secrets engines starting with this name
    auth = list(object({
      auth_path            = string                       // The auth path to use or create
      auth_id              = optional(string, "")         // AWS customer account number, k8s service account namespace, domain for kerberos, etc
      access               = optional(string, "")         // AWS access key for IAM user, k8s service account name
      rolenames            = optional(list(string), [""]) // This will be the IAM roles that get read access
      bound_cidrs          = optional(list(string), [])   // This will set any token or secret bound cidr lists
      bind_secret_id       = optional(bool, true)         // WARNING if set to "false" it will not require a secret to login
      valid_boundaries     = list(string)
      custom_reader_policy = optional(string, "")
    }))
    create_shared = bool            // If true, we will create a "shared" engine and policies for this app
    engine_types  = list(string)    // Must match a defined type of secret engine, to be created
    editor_groups = list(object({   //
      editor_group_name    = string // Must match the Okta group name for secret editors, see note below
      editor_group_subpath = string // This is the subpath within an engine that a group has access to.
      // If a group should have access to all subpaths in an engine (default/admin group), use "*".
    }))
  }))
  default = [
  ]
}

It also fails if I make the default null which I am going to assume is a bug in the vault TF provider. I have a support ticket open but figured this would be good to post here for visibility.

Aha. I think I have found that the problem was entirely on my end. I had another intermediate variable doing a questionable can() statement; when I cleaned that up, setting the default to null also started working.

# bound_cidrs    = can("${auth.bound_cidrs}") ? "${auth.bound_cidrs}" : null         
bound_cidrs    = "${auth.bound_cidrs}"
  readers = flatten([
    for app in var.standard_apps : [
      for auth in app.auth : [
        for boundary in auth.valid_boundaries : {
          app_name       = "${app.app_name}"
          app_boundary   = "${app.app_name}_${boundary}"
          group_name     = "hcp_vault_app_${app.app_name}_${boundary}_users"
          auth_path      = "${auth.auth_path}"
          auth_id        = can("${auth.auth_id}") ? "${auth.auth_id}" : null
          access         = can("${auth.access}") ? "${auth.access}" : null
          bound_cidrs    = can("${auth.bound_cidrs}") ? "${auth.bound_cidrs}" : null
          bind_secret_id = can("${auth.bind_secret_id}") ? "${auth.bind_secret_id}" : null
          # bound_cidrs    = can("${auth.bound_cidrs}") ? "${auth.bound_cidrs}" : null
          bound_cidrs    = "${auth.bound_cidrs}"
          bind_secret_id = can("${auth.bind_secret_id}") ? "${auth.bind_secret_id}" : null          
          rolenames      = can("${auth.rolenames}") ? "${auth.rolenames}" : null
          // If there's no custom policy, use the calculated ones
          policies       = length(trimspace(auth.custom_reader_policy)) > 0 ? [auth.custom_reader_policy] : lookup(local.all_reader_policies, "${app.app_name}_${boundary}", [])
        }
      ]
    ]
  ])

1 Like

Thanks for reporting back on this!