google-cloud-platformterraformterraform-provider-gcpgoogle-cloud-monitoring

How to call the value of a map (object) resource in another map (object) resource



resource "google_monitoring_custom_service" "customsrv" {
  for_each = var.custom_service_level
  display_name = each.value.service_display_name
  service_id = each.value.service_id
  project = var.project_id
}

// Google Monitoring SLO objects can support many different metric types, for more info see our documenation. 
resource "google_monitoring_slo" "custom_request_based_slo" {
   for_each = var.custom_sli
   service = google_monitoring_custom_service.customsrv[each.key].service_id
  display_name = each.value.metric_display_name
  goal = each.value.goal
  rolling_period_days = each.value.rolling_period_days // Replacable with calendar_period = "DAY", "WEEK", "FORTNIGHT", or "MONTH"
  request_based_sli {
    // Alternate implementation could use distribution_cut instead of good_total_ratio. 
    good_total_ratio {
      // Any combination of two elements: good_service_filter, bad_service_filter, total_service_filter. 
      good_service_filter = join(" AND ", each.value.good_service_filter)
      total_service_filter = join(" AND ", each.value.total_service_filter)
    }
  }
  depends_on = [ google_monitoring_custom_service.customsrv ]
}

Using .tfvars values:

custom_service_level = {
  "composer-service" = {
    service_id = "custom-srv-slos"
    service_display_name = "My Custom SLO"    
  }
}

custom_sli = {
  "composer-health" = { 
    metric_display_name = "test slo with service based SLI"
    goal = 0.9
    rolling_period_days = 28
    window_period = "300s"
    good_service_filter = [
          "metric.type=\"composer.googleapis.com/workflow/run_count\"",
          "resource.type=\"cloud_composer_workflow\"",
          "metric.labels.state=\"success\"",
      ],
    total_service_filter = [
          "metric.type=\"composer.googleapis.com/workflow/run_count\"",
          "resource.type=\"cloud_composer_workflow\"",
      ]
  },
}

Variables used:

variable "custom_service_level" {
  type = map(object({
    service_id = string,
    service_display_name = string,
  })) 
} 

variable "custom_sli" {
  type = map(object({
     metric_display_name = string,
     goal = number,
     rolling_period_days = number,
     good_service_filter = list(string),
     total_service_filter = list(string)
    # service = string   
  }))
}

Getting this error on plan:

│ Error: Invalid index
│
│   on main.tf line 600, in resource "google_monitoring_slo" "custom_request_based_slo":
│  600:    service = google_monitoring_custom_service.customsrv[each.key].service_id
│     ├────────────────
│     │ each.key is "composer-health"
│     │ google_monitoring_custom_service.customsrv is object with 1 attribute "composer-service"
│
│ The given key does not identify an element in this collection value.
╵

getting errors while creating alerts for above every above created map(object) both resource(1:1 relationship) using workaround1

resource "google_monitoring_alert_policy" "slo_alerts" {
  project = var.project_id
  display_name = var.slo_alert_display_name
  combiner     = "OR"

  conditions {
    display_name = var.slo_alert_display_name
    condition_threshold {
    filter = "select_slo_burn_rate(\"projects/${var.project_id}/services/${google_monitoring_custom_service.customsrv.service_id}/serviceLevelObjectives/${google_monitoring_slo.custom_request_based_slo.slo_id[]}\", \"3600s\")"
      duration   = var.duration
      comparison = "COMPARISON_GT"
      threshold_value = var.threshold_value
    }
  }
  notification_channels =  [
    for channel in google_monitoring_notification_channel.basic : channel.name
  ]

  documentation {
    content = var.content
  }
}

variables used:-

variable "enabled" {
  type = bool
  default = "true"
}

variable "slo_alert_display_name" {
type = string
default = "SLO Burn Rate Alert"
}

variable "content" {
type = string
default = "SLO burn for the past 60min exceeded x10 times the acceptable budget burn rate. Please verify from the console and take necessary action."
}

variable "threshold_value" {
type = number
default = 10
}

variable "duration" {
  type = string
default = "0s"
}

error getting on plan

PS C:\tf-workspace testrepo\testrepo1> terraform plan
Acquiring state lock. This may take a few moments...
╷
│ Error: Reference to "each" in context without for_each
│
│   on logging-alert-policy.tf line 11, in resource "google_monitoring_alert_policy" "slo_alerts":
│   11:       filter = "select_slo_burn_rate(\"projects/${var.project_id}/services/${google_monitoring_custom_service.customsrv[each.key].service_id}/serviceLevelObjectives/${google_monitoring_slo.custom_request_based_slo[each.key].slo_id}\", \"3600s\")"
│
│ The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.
╵
╷
│ Error: Reference to "each" in context without for_each
│
│   on logging-alert-policy.tf line 11, in resource "google_monitoring_alert_policy" "slo_alerts":
│   11:       filter = "select_slo_burn_rate(\"projects/${var.project_id}/services/${google_monitoring_custom_service.customsrv[each.key].service_id}/serviceLevelObjectives/${google_monitoring_slo.custom_request_based_slo[each.key].slo_id}\", \"3600s\")"
│
│ The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.

how to use map(objects) for alert policy resource for each (customsrv and ustom_request_based_slo)


Solution

  • The problem

    You are defining 2 maps, each one with different keys - composer-service and composer-health:

    custom_service_level = {
      "composer-service" = { # <--------------------- map key
        # ...
      }
    }
    
    custom_sli = {
      "composer-health" = { # <--------------------- map key
        # ...
      }
    }
    

    The following line will throw an error because there is no google_monitoring_custom_service.customsrv resource with key composer-health:

    service = google_monitoring_custom_service.customsrv[each.key].service_id
    

    Workaround 1

    If there is a 1:1 relationship between custom_service_level and custom_sli you can use the same keys in both maps.

    Working example using null_resource:

    variable "custom_service_level" {
      type = map(object({
        service_id           = string,
        service_display_name = string,
      }))
      default = {
        "composer-001" = { // <-------------------- map key
          service_id           = "custom-srv-slos"
          service_display_name = "My Custom SLO"
        }
      }
    }
    
    variable "custom_sli" {
      type = map(object({
        metric_display_name  = string,
        goal                 = number,
        rolling_period_days  = number,
        good_service_filter  = list(string),
        total_service_filter = list(string)
      }))
      default = {
        "composer-001" = { // <-------------------- same map key used in var.custom_service_level
          metric_display_name = "test slo with service based SLI"
          goal                = 0.9
          rolling_period_days = 28
          window_period       = "300s"
          good_service_filter = [
            "metric.type=\"composer.googleapis.com/workflow/run_count\"",
            "resource.type=\"cloud_composer_workflow\"",
            "metric.labels.state=\"success\"",
          ],
          total_service_filter = [
            "metric.type=\"composer.googleapis.com/workflow/run_count\"",
            "resource.type=\"cloud_composer_workflow\"",
          ]
        },
      }
    }
    
    resource "null_resource" "customsrv" {
      for_each = var.custom_service_level
    
      triggers = {
        display_name = each.value.service_display_name
        service_id   = each.value.service_id
      }
    }
    
    resource "null_resource" "custom_request_based_slo" {
      for_each = var.custom_sli
    
      triggers = {
        service             = null_resource.customsrv[each.key].triggers.service_id
        display_name        = each.value.metric_display_name
        goal                = each.value.goal
        rolling_period_days = each.value.rolling_period_days
      }
    }
    

    Running terraform plan:

    Terraform used the selected providers to generate the following execution
    plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # null_resource.custom_request_based_slo["composer-001"] will be created
      + resource "null_resource" "custom_request_based_slo" {
          + id       = (known after apply)
          + triggers = {
              + "display_name"        = "test slo with service based SLI"
              + "goal"                = "0.9"
              + "rolling_period_days" = "28"
              + "service"             = "custom-srv-slos"
            }
        }
    
      # null_resource.customsrv["composer-001"] will be created
      + resource "null_resource" "customsrv" {
          + id       = (known after apply)
          + triggers = {
              + "display_name" = "My Custom SLO"
              + "service_id"   = "custom-srv-slos"
            }
        }
    
    Plan: 2 to add, 0 to change, 0 to destroy.
    

    Workaround 2

    Add a custom_service_level property to var.custom_sli. This property must contain one of the map keys of var.custom_service_level.

    Working example using null_resource:

    variable "custom_service_level" {
      type = map(object({
        service_id           = string,
        service_display_name = string,
      }))
      default = {
        "composer-service" = { // <-------------------- map key
          service_id           = "custom-srv-slos"
          service_display_name = "My Custom SLO"
        }
      }
    }
    
    variable "custom_sli" {
      type = map(object({
        custom_service_level = string, // <------------------ new property
        metric_display_name  = string,
        goal                 = number,
        rolling_period_days  = number,
        good_service_filter  = list(string),
        total_service_filter = list(string)
      }))
      default = {
        "composer-health" = {
          custom_service_level = "composer-service" // <-------------------- map key from var.custom_service_level
          metric_display_name  = "test slo with service based SLI"
          goal                 = 0.9
          rolling_period_days  = 28
          window_period        = "300s"
          good_service_filter = [
            "metric.type=\"composer.googleapis.com/workflow/run_count\"",
            "resource.type=\"cloud_composer_workflow\"",
            "metric.labels.state=\"success\"",
          ],
          total_service_filter = [
            "metric.type=\"composer.googleapis.com/workflow/run_count\"",
            "resource.type=\"cloud_composer_workflow\"",
          ]
        },
      }
    }
    
    resource "null_resource" "customsrv" {
      for_each = var.custom_service_level
    
      triggers = {
        display_name = each.value.service_display_name
        service_id   = each.value.service_id
      }
    }
    
    resource "null_resource" "custom_request_based_slo" {
      for_each = var.custom_sli
    
      triggers = {
        service             = null_resource.customsrv[each.value.custom_service_level].triggers.service_id
        display_name        = each.value.metric_display_name
        goal                = each.value.goal
        rolling_period_days = each.value.rolling_period_days
      }
    }
    

    Running terraform plan:

    Terraform used the selected providers to generate the following execution
    plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # null_resource.custom_request_based_slo["composer-health"] will be created
      + resource "null_resource" "custom_request_based_slo" {
          + id       = (known after apply)
          + triggers = {
              + "display_name"        = "test slo with service based SLI"
              + "goal"                = "0.9"
              + "rolling_period_days" = "28"
              + "service"             = "custom-srv-slos"
            }
        }
    
      # null_resource.customsrv["composer-service"] will be created
      + resource "null_resource" "customsrv" {
          + id       = (known after apply)
          + triggers = {
              + "display_name" = "My Custom SLO"
              + "service_id"   = "custom-srv-slos"
            }
        }
    
    Plan: 2 to add, 0 to change, 0 to destroy.