hashicorp / terraform-provider-google

Terraform Provider for Google Cloud Platform
https://registry.terraform.io/providers/hashicorp/google/latest/docs
Mozilla Public License 2.0
2.29k stars 1.73k forks source link

Always in-place updates for google_billing_budget #8444

Open cschroer opened 3 years ago

cschroer commented 3 years ago

Community Note

Terraform Version

❯ terraform -v
Terraform v0.14.6
+ provider registry.terraform.io/hashicorp/google v3.56.0
+ provider registry.terraform.io/hashicorp/google-beta v3.56.0
+ provider registry.terraform.io/hashicorp/null v3.0.0

Affected Resource(s)

Terraform Configuration Files

variable "projects" {
  description = <<-EOD
    A set of associated projects for the budget ({project_id} format). If omitted, the report will
    include all usage for the billing account, regardless of which project the usage occurred on.
  EOD
  type        = set(string)
  default     = null
}

variable "services" {
  description = <<-EOD
    A set of associated services for the budget ({service_id} format). Only usage from this set will
    be included in the budget. If omitted, the report will include usage for all services.
  EOD
  type        = set(string)
  default     = null
}

variable "threshold_rules" {
  description = "List of threshold rules (map with threshold_percent and spend_basis keys)"
  type        = list(object({ threshold_percent = number, spend_basis = string }))
  default = [
    {
      threshold_percent = 1,
      spend_basis       = "FORECASTED_SPEND"
    },
    {
      threshold_percent = 1,
      spend_basis       = "CURRENT_SPEND"
    }
  ]
}

data "google_project" "project" {
  provider   = google
  for_each   = var.projects
  project_id = each.key
}

resource "google_billing_budget" "budget" {
  provider        = google
  billing_account = var.billing_account
  display_name    = "${var.budget_prefix}-${var.name}"

  budget_filter {
    projects               = try([for project in data.google_project.project : "projects/${project.number}"], [])
    services               = try([for service in var.services : "services/${service}"], [])
    credit_types_treatment = var.credit_types_treatment
  }

  amount {
    specified_amount {
      units         = var.budget_amount
      currency_code = var.budget_currency
    }
  }

  dynamic "threshold_rules" {
    for_each = var.threshold_rules
    iterator = rule

    content {
      threshold_percent = rule.value.threshold_percent
      spend_basis       = rule.value.spend_basis
    }
  }

  all_updates_rule {
    pubsub_topic = "projects/${var.pubsub_target_project}/topics/${var.pubsub_target_topic}"
  }
}

Expected Behavior

After apply terraform should detect no change is needed.

Actual Behavior

Terraform does an in-place update every run:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_billing_budget.budget will be updated in-place
  ~ resource "google_billing_budget" "budget" {
        id              = "billingAccounts/********/budgets/********"
        name            = "billingAccounts/********/budgets/********"
        # (2 unchanged attributes hidden)

      ~ budget_filter {
          ~ projects               = [
              + "projects/12345678",
                "projects/23456789",
                "projects/34567890",
              - "projects/12345678",
            ]
            # (5 unchanged attributes hidden)
        }

      ~ threshold_rules {
          ~ spend_basis       = "CURRENT_SPEND" -> "FORECASTED_SPEND"
            # (1 unchanged attribute hidden)
        }
      ~ threshold_rules {
          ~ spend_basis       = "FORECASTED_SPEND" -> "CURRENT_SPEND"
            # (1 unchanged attribute hidden)
        }
        # (2 unchanged blocks hidden)
    }

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

Seams like using variables and dynamic blocks like we do in our module results in an in-place update. The order inside budget_filter projects isn't ignored, so it get's updated every time. Also the threshold_rules may have a different order in state and API return. This should be ignored, too

Steps to Reproduce

  1. terraform apply

References

rileykarson commented 3 years ago

This also appears to be returning project numbers instead of ids, see https://github.com/terraform-google-modules/terraform-google-project-factory/issues/544#issuecomment-777968811. Let's fix that at the same time.

      ~ budget_filter {
            credit_types           = []
            credit_types_treatment = "INCLUDE_ALL_CREDITS"
            labels                 = {}
          ~ projects               = [
              - "projects/1234567809",
              + "projects/prj-test-ffc8123454",
            ]
            services               = []
            subaccounts            = []
        }
slevenick commented 3 years ago

This looks like it changed recently on the API-side. I have filed an issue against the upstream team and am waiting on a resolution. It is difficult to go from project number -> project id within the provider, and I would like to avoid attempting to handle this in a DiffSuppressFunc if possible.

cschroer commented 3 years ago

Hey, is there any update on this?

abrahammartin commented 3 years ago

I can confirm that we have the same problem described in the issue

abrahammartin commented 3 years ago

This looks like it changed recently on the API-side. I have filed an issue against the upstream team and am waiting on a resolution. It is difficult to go from project number -> project id within the provider, and I would like to avoid attempting to handle this in a DiffSuppressFunc if possible.

We are using a set of project numbers as described in the documentation but responses from the API don't seem to be idempotent as Terraform wants to change order at every re-apply.

cschroer commented 3 years ago

Is this still an upstream error? For me it mostly looks like the terraform provider assumes a certain order for those project IDs and threshold_rules blocks and this order doesn't match the APIs responses.

silvpol commented 3 years ago

I have come across the same issue, it looks like the projects are first sorted by the parent id/number (folder or org) and then by the project number. I have used the code below and so far the diffs are gone.

data "google_projects" "regional" {
  filter = "labels.region:${lower(var.region)}"
}

data "google_project" "regional" {
  count      = length(data.google_projects.regional.projects[*].project_id)
  project_id = data.google_projects.regional.projects[count.index].project_id
}

locals {
  projects_map           = {for project in data.google_projects.regional.projects[*]: "${project.parent.id}|${project.number}" => project.number}
  sorted_project_numbers = [for key in sort(keys(local.projects_map)) : local.projects_map[key]]
}
alvaro-gh commented 3 years ago

I have come across the same issue, it looks like the projects are first sorted by the parent id/number (folder or org) and then by the project number. I have used the code below and so far the diffs are gone.

data "google_projects" "regional" {
  filter = "labels.region:${lower(var.region)}"
}

data "google_project" "regional" {
  count      = length(data.google_projects.regional.projects[*].project_id)
  project_id = data.google_projects.regional.projects[count.index].project_id
}

locals {
  projects_map           = {for project in data.google_projects.regional.projects[*]: "${project.parent.id}|${project.number}" => project.number}
  sorted_project_numbers = [for key in sort(keys(local.projects_map)) : local.projects_map[key]]
}

I had no idea google_projects (plural) data source existed. My two cents for this and sorry for the complex locals:

data "google_projects" "company_projects" {
  for_each   = toset(var.company_projects)
  filter     = "id:${each.value}"
}

locals {
  # This is a list of maps
  projects_map_list = flatten([
    for instance in data.google_projects.company_projects : [
      for project in instance.projects : {
        project_parent = project.parent.id
        project_number = project.number
      }
    ]
  ])

  # This is a map
  projects_map = { for e in local.projects_map_list : "${e.project_parent}/${e.project_number}" => e.project_number }

  # This is the final list
  sorted_projects = [ for key in sort(keys(local.projects_map)) : local.projects_map[key] ]
}

The variable company_projects is a list of strings with the project IDs. Then I used sorted_projects like this:

budget_filter {
    projects = [ for p in local.sorted_projects : "projects/${p}"]
}

And this didn't work for me after all :(

silvpol commented 3 years ago

That's odd, I have tested the workaround and no diffs show. We label all projects per region so that list of projects is kept up to date automatically. Below is the full example of what we use.

data "google_projects" "regional" {
  filter = "labels.region:${lower(var.region)}"
}

data "google_project" "regional" {
  count      = length(data.google_projects.regional.projects[*].project_id)
  project_id = data.google_projects.regional.projects[count.index].project_id
}

locals {
  projects_map           = {for project in data.google_projects.regional.projects[*]: "${project.parent.id}|${project.number}" => project.number}
  sorted_project_numbers = [for key in sort(keys(local.projects_map)) : local.projects_map[key]]
}

resource "google_billing_budget" "regional" {
  billing_account = var.billing_account
  display_name    = var.region
  count           = length(data.google_projects.regional.projects[*].project_id)>0?1:0

  budget_filter {
    projects = formatlist("projects/%s", local.sorted_project_numbers)
  }

  amount {
    last_period_amount = true
  }

  threshold_rules {
    threshold_percent = 1.01
  }

  threshold_rules {
    threshold_percent = 1.05
    spend_basis       = "FORECASTED_SPEND"
  }
}

N.B. Using google provider 3.60, as later has a DNS resource bug. Using Terraform v0.12.31 in case it makes any difference.

asychev commented 3 years ago

Seems like it is fixed in 3.77.0?

iamgeef commented 2 years ago

I'm having the same issue on 4.1.0 (with TF 0.13.0)

My TF:

threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "FORECASTED_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.75
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.75
    spend_basis       = "FORECASTED_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "FORECASTED_SPEND"
  }

  threshold_rules {
    threshold_percent = 1
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 1
    spend_basis       = "FORECASTED_SPEND"
  }

Actual: image

TF-Apply: image

TF State File:

"threshold_rules": [
              {
                "spend_basis": "CURRENT_SPEND",
                "threshold_percent": 0.5
              },
              {
                "spend_basis": "CURRENT_SPEND",
                "threshold_percent": 0.75
              },
              {
                "spend_basis": "CURRENT_SPEND",
                "threshold_percent": 0.9
              },
              {
                "spend_basis": "CURRENT_SPEND",
                "threshold_percent": 1
              },
              {
                "spend_basis": "FORECASTED_SPEND",
                "threshold_percent": 0.5
              },
              {
                "spend_basis": "FORECASTED_SPEND",
                "threshold_percent": 0.75
              },
              {
                "spend_basis": "FORECASTED_SPEND",
                "threshold_percent": 0.9
              },
              {
                "spend_basis": "FORECASTED_SPEND",
                "threshold_percent": 1
              }
            ],

The Billing API is grouping each rule into the _spendbasis category (even though the console doesn't display it like that).

API Result:

"thresholdRules": [
    {
      "thresholdPercent": 0.5,
      "spendBasis": "CURRENT_SPEND"
    },
    {
      "thresholdPercent": 0.75,
      "spendBasis": "CURRENT_SPEND"
    },
    {
      "thresholdPercent": 0.9,
      "spendBasis": "CURRENT_SPEND"
    },
    {
      "thresholdPercent": 1,
      "spendBasis": "CURRENT_SPEND"
    },
    {
      "thresholdPercent": 0.5,
      "spendBasis": "FORECASTED_SPEND"
    },
    {
      "thresholdPercent": 0.75,
      "spendBasis": "FORECASTED_SPEND"
    },
    {
      "thresholdPercent": 0.9,
      "spendBasis": "FORECASTED_SPEND"
    },
    {
      "thresholdPercent": 1,
      "spendBasis": "FORECASTED_SPEND"
    }

Even if I change my Terraform to group the rules into their spendBasis category it still returns a change:

New TF:

threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.75
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 1
    spend_basis       = "CURRENT_SPEND"
  }

  threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "FORECASTED_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.75
    spend_basis       = "FORECASTED_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "FORECASTED_SPEND"
  }
  threshold_rules {
    threshold_percent = 1
    spend_basis       = "FORECASTED_SPEND"
  }

New TF Result: image

iamgeef commented 1 year ago

Still experiencing the above issue as of today - any updates or is still pending upstream changes? I tell a lie...ordering the threshold rules by spend_basis, no longer shows up as drift. e.g group all CURRENT_SPEND together, followed by FORECASTED_SPEND.

threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.75
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 1
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "FORECASTED_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.75
    spend_basis       = "FORECASTED_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "FORECASTED_SPEND"
  }
  threshold_rules {
    threshold_percent = 1
    spend_basis       = "FORECASTED_SPEND"
  }
gabor-farkas commented 1 year ago

@iamgeef thanks for the tip, this worked. Just one addition, it matters whether we specified the threshold percent as a string or a number :) If I add a threshold rule with a percent specified as a string, it gets sorted by the API after the numerically-specified values (inside the spend_basis group).