Can't get syntax right for groups in google's load-balancer code

I have a Terraform setup from Google’s load-balancer stuff … I have it all going except for one piece of syntax. It’s used in the code like this:

resource "google_compute_url_map" "default" {
  project         = var.project
  count           = var.create_url_map ? 1 : 0
  name            = "${var.name}-url-map"
  default_service = google_compute_backend_service.default[keys(var.backends)[0]].self_link 
 }


 dynamic "backend" {
    for_each = toset(each.value["groups"])
     content {
       description = lookup(backend.value, "description", null)
       group       = lookup(backend.value, "group")

    }
  }

I’ve tried all sorts of things for syntax, none of which have worked yet, for example:

 groups = [{group = "google_compute_instance_group.vm_group.id"}]

Any idea of the right syntax? Thanks much for any guidance whatsoever!

Hi @mark.mcwiggins,

The indentation in your example is a bit inconsistent so I tidied it up so I could more easily read the nesting in your example:

resource "google_compute_url_map" "default" {
  project         = var.project
  count           = var.create_url_map ? 1 : 0
  name            = "${var.name}-url-map"
  default_service = google_compute_backend_service.default[keys(var.backends)[0]].self_link 
}

dynamic "backend" {
  for_each = toset(each.value["groups"])
  content {
    description = lookup(backend.value, "description", null)
    group       = lookup(backend.value, "group")
  }
}

From that it seems like you have the dynamic "backend" block at the top-level rather than nested inside the resource block, and that isn’t a valid place for a dynamic block to be.

I’m not really familiar with this google_compute_url_map resource type, but it seems from its documentation that it doesn’t expect a nested block of type backend, so I don’t think moving that dynamic block to be nested inside would be a sufficient answer either.

Could you perhaps share an example of what you’d expect this configuration to look like if you weren’t using a dynamic block and were instead manually writing out a backend block for each of the “groups” in your system, and also show the value of that “groups” attribute? From that, I can hopefully show you an example of adapting that to be dynamic based on the content of your “groups” collection.

Hi … you’re right, the dynamic “backend” is misplaced; it lives inside this one:

    resource "google_compute_backend_service" "default" {. 
      provider = google-beta
      for_each = var.backends

      project = var.project
      name    = "${var.name}-backend-${each.key}"


      description                     = lookup(each.value, "description", null)
      connection_draining_timeout_sec = lookup(each.value, "connection_draining_timeout_sec", null)
      enable_cdn                      = lookup(each.value, "enable_cdn", false)
      custom_request_headers          = lookup(each.value, "custom_request_headers", [])
      custom_response_headers         = lookup(each.value, "custom_response_headers", [])

      # To achieve a null backend security_policy, set each.value.security_policy to "" (empty string), otherwise, it fallsback to var.security_policy.
      security_policy = lookup(each.value, "security_policy") == "" ? null : (lookup(each.value, "security_policy") == null ? var.security_policy : each.value.security_policy)

      dynamic "backend" {
        for_each = toset(each.value["groups"])
        content {
          description = lookup(backend.value, "description", null)
          group       = lookup(backend.value, "group")

        }
      }


  log_config {
    enable      = lookup(lookup(each.value, "log_config", {}), "enable", true)
    sample_rate = lookup(lookup(each.value, "log_config", {}), "sample_rate", "1.0")
  }

  dynamic "iap" {
    for_each = lookup(lookup(each.value, "iap_config", {}), "enable", false) ? [1] : []
    content {
      oauth2_client_id     = lookup(lookup(each.value, "iap_config", {}), "oauth2_client_id", "")
      oauth2_client_secret = lookup(lookup(each.value, "iap_config", {}), "oauth2_client_secret", "")
    }
    }


    }

I just haven’t come up with the syntax in the variables file:

Error: Rrror creating BackendService: googleapi: Error 400: Invalid value for field 'resource.backends[0].group': 'google_compute_backend_service.default.id'. The URL is malformed., invalid

The complete file:

    http_forward = true
    https_redirect = true
    create_address = true
    project = "my-dev"
    backends = {
        "yobaby" = {
                 description = "my app"
	         enable_cdn = false
	        security_policy = ""
	         custom_request_headers = null
	         custom_response_headers = null
	         iap_config = {
	             enable = false
	  	     oauth2_client_id     = ""
                     oauth2_client_secret = ""
		     }
	          log_config = {
	             enable = false
		     sample_rate = 0
	      	 }
	        	 groups = [{group = "google_compute_backend_service.default.id"}]
        	    }
        	    }

Syntax please?

Hi @mark.mcwiggins,

Again the formatting seems to have got messed up as you’ve transferred this into the forum, so I’m gonna post a tidied version so we can see what’s going on here:

resource "google_compute_backend_service" "default" {
  provider = google-beta
  for_each = var.backends

  project = var.project
  name    = "${var.name}-backend-${each.key}"

  description                     = lookup(each.value, "description", null)
  connection_draining_timeout_sec = lookup(each.value, "connection_draining_timeout_sec", null)
  enable_cdn                      = lookup(each.value, "enable_cdn", false)
  custom_request_headers          = lookup(each.value, "custom_request_headers", [])
  custom_response_headers         = lookup(each.value, "custom_response_headers", [])

  # To achieve a null backend security_policy, set each.value.security_policy to "" (empty string), otherwise, it fallsback to var.security_policy.
  security_policy = lookup(each.value, "security_policy") == "" ? null : (lookup(each.value, "security_policy") == null ? var.security_policy : each.value.security_policy)

  dynamic "backend" {
    for_each = toset(each.value["groups"])
    content {
      description = lookup(backend.value, "description", null)
      group       = lookup(backend.value, "group")
    }
  }

  log_config {
    enable      = lookup(lookup(each.value, "log_config", {}), "enable", true)
    sample_rate = lookup(lookup(each.value, "log_config", {}), "sample_rate", "1.0")
  }

  dynamic "iap" {
    for_each = lookup(lookup(each.value, "iap_config", {}), "enable", false) ? [1] : []
    content {
      oauth2_client_id     = lookup(lookup(each.value, "iap_config", {}), "oauth2_client_id", "")
      oauth2_client_secret = lookup(lookup(each.value, "iap_config", {}), "oauth2_client_secret", "")
    }
  }
}
http_forward   = true
https_redirect = true
create_address = true
project        = "my-dev"
backends = {
  "yobaby" = {
    description             = "my app"
    enable_cdn              = false
    security_policy         = ""
    custom_request_headers  = null
    custom_response_headers = null
    iap_config = {
      enable               = false
      oauth2_client_id     = ""
      oauth2_client_secret = ""
    }
    log_config = {
      enable      = false
      sample_rate = 0
    }
    groups = [
      {
        group = "google_compute_backend_service.default.id"
      },
    ]
  }
}

I’m guessing that the second block you included here is the content of a .tfvars file you’re setting, or of a module block where you called this module, and so the backends map in there is what you’ve assigned to the resource for_each in the first example.

I see inside your groups list you’ve written a string containing something resembling a Terraform reference, but since it’s in quotes Terraform is just taking the literal string "google_compute_backend_service.default.id" and assigning that to the group argument, and so the remote API is rejecting it as an invalid URL.

I assume your intent here was to assign the value of the expression google_compute_backend_service.default.id. If so, you’ll need to write that expression inside the module itself rather than inside the variables file, because the variables are outside the module, and so can’t refer to anything inside the module.

However, if I’m reading correctly then inlining that reference would cause the configuration of google_compute_backend_service.default to be referring to itself, which isn’t possible because it requires Terraform to already know the result of creating that object before creating the object.

Given that, I think we need to take a step back here and first talk about the underlying problem you’re trying to solve. I’m afraid I’m not familiar with this resource type at all so I can’t infer from context why it would be necessary to use this object’s own ID as part of it’s own configuration; are you sure that’s the correct way to use this resource type? The documentation for this backend block’s group argument says:

  • group - (Required) The fully-qualified URL of an Instance Group or Network Endpoint Group resource. In case of instance group this defines the list of instances that serve traffic. Member virtual machine instances from each instance group must live in the same zone as the instance group itself. No two backends in a backend service are allowed to use same Instance Group resource. For Network Endpoint Groups this defines list of endpoints. All endpoints of Network Endpoint Group must be hosted on instances located in the same zone as the Network Endpoint Group. Backend services cannot mix Instance Group and Network Endpoint Group backends. Note that you must specify an Instance Group or Network Endpoint Group resource using the fully-qualified URL, rather than a partial URL.

This suggests to me that this argument is expecting the URL/id of some other object… perhaps an instance of google_compute_instance_group or of compute_network_endpoint_group? If so, I think what you’d need to do here is make your variable include a lookup key for whatever other object it’s supposed to refer to, and then write an expression to look up the object using that key.

For example, perhaps inside your backends variable you’d write something like this:

backends = {
  "yobaby" = {
    # (everything else unchanged)
    groups = [
      {
        instance_group_key = "foo"
      },
    ]
  }
}

Then, in your resource configuration:

  dynamic "backend" {
    for_each = each.value["groups"]
    content {
      description = try(backend.value.description, null)
      group       = google_compute_instance_group.example[backend.value.instance_group_key].id
    }
  }

This assumes that you have a resource "google_compute_instance_group" "example" block elsewhere in your module which also uses for_each, and that “foo” is one of the keys in that resource’s for_each expression. The input variable therefore specifies which of the instances of that resource to use, by including the lookup key by which we can find the corresponding id.

The Terraform setup from Google is set to use Terraform 0.13, so after first failing with 1.0.4 I backtracked and am using that.

I would expect the Google code to work as is without changing, just putting in the variables in the .tfvars file, and all I am misssing is the syntax for the ‘groups’ item in question.

The resource configuration is almost exactly as you specified above:

dynamic "backend" {
    for_each = toset(each.value["groups"])
    content {
      description = lookup(backend.value, "description", null)
      group       = lookup(backend.value, "group")

    }

Any further suggestions knowing this? Thanks much for your help!

Unless you can hard-code a group identifier in the variable you’re using to populate this, there is no path forward that doesn’t involve some indirection so that you can look up the dynamically-chosen identifier (which isn’t known yet at the time Terraform is evaluating the input variables).