From 4879b25d1c6a3910c7ac1fc3fd00432befeb55a7 Mon Sep 17 00:00:00 2001 From: Ludo Date: Tue, 10 Oct 2023 11:12:49 +0200 Subject: [PATCH 1/6] initial untested draft --- modules/billing-account/budgets.tf | 128 +++++++++++++++++ modules/billing-account/iam.tf | 70 ++++++++++ modules/billing-account/logging.tf | 92 +++++++++++++ modules/billing-account/main.tf | 21 +++ modules/billing-account/outputs.tf | 0 modules/billing-account/variables.tf | 198 +++++++++++++++++++++++++++ modules/billing-account/versions.tf | 29 ++++ modules/folder/iam.tf | 2 +- modules/folder/variables.tf | 2 +- 9 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 modules/billing-account/budgets.tf create mode 100644 modules/billing-account/iam.tf create mode 100644 modules/billing-account/logging.tf create mode 100644 modules/billing-account/main.tf create mode 100644 modules/billing-account/outputs.tf create mode 100644 modules/billing-account/variables.tf create mode 100644 modules/billing-account/versions.tf diff --git a/modules/billing-account/budgets.tf b/modules/billing-account/budgets.tf new file mode 100644 index 0000000000..9ef7a2c9eb --- /dev/null +++ b/modules/billing-account/budgets.tf @@ -0,0 +1,128 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_monitoring_notification_channel" "default" { + for_each = var.budget_notification_channels + description = each.value.description + display_name = ( + each.value.display_name != null + ? each.value.display_name + : "Budget email notification ${each.key}." + ) + project = each.value.project_id + enabled = each.value.enabled + force_delete = each.value.force_delete + type = each.value.tupe + labels = each.value.labels + user_labels = each.value.user_labels + dynamic "sensitive_labels" { + for_each = toset(coalesce(each.value.sensitive.labels, {})) + content { + auth_token = sensitive_labels.value.auth_token + password = sensitive_labels.value.password + service_key = sensitive_labels.value.service_key + } + } +} + +resource "google_billing_budget" "default" { + for_each = var.budgets + billing_account = var.id + display_name = each.value.display_name + dynamic "amount" { + for_each = each.value.amount.use_last_period == true ? [""] : [] + content { + last_period_amount = true + } + } + dynamic "amount" { + for_each = each.value.amount.use_last_period != true ? [""] : [] + content { + currency_code = each.value.amount.currency_code + nanos = each.value.amount.nanos + units = each.value.amount.units + } + } + budget_filter { + calendar_period = try(each.value.period.calendar, null) + credit_types_treatment = ( + try(each.value.credit_types_treatment.exclude_all, null) == true + ? "EXCLUDE_ALL_CREDITS" + : ( + try(each.value.credit_types_treatment.include_specified, null) != null + ? "INCLUDE_SPECIFIED_CREDITS" + : "INCLUDE_ALL_CREDITS" + ) + ) + labels = each.value.label == null ? null : { + (each.value.label.key) = each.value.label.value + } + projects = each.value.projects + resource_ancestors = each.value.resource_ancestors + services = each.value.services + subaccounts = each.value.subaccounts + dynamic "custom_period" { + for_each = try(each.value.period.custom, null) != null ? [""] : [] + content { + start_date { + day = each.value.period.custom.start_date.day + month = each.value.period.custom.start_date.month + year = each.value.period.custom.start_date.year + } + dynamic "end_date" { + for_each = try(each.value.period.custom.end_date, null) != null ? [""] : [] + content { + day = each.value.period.custom.end_date.day + month = each.value.period.custom.end_date.month + year = each.value.period.custom.end_date.year + } + } + } + } + } + dynamic "threshold_rules" { + for_each = each.value.threshold_rules + iterator = rule + content { + threshold_percent = rule.value.percent + spend_basis = ( + rule.value.forecasted_spend + ? "FORECASTED_SPEND" + : "CURRENT_SPEND" + ) + } + } + dynamic "all_updates_rule" { + for_each = each.value.all_update_rules + iterator = rule + content { + monitoring_notification_channels = [ + for v in rule.value.monitoring_notification_channels : try( + google_monitoring_notification_channel[v].id, v + ) + ] + pubsub_topic = var.pubsub_topic + # disable_default_iam_recipients can only be set if + # monitoring_notification_channels is nonempty + disable_default_iam_recipients = ( + try(length(rule.value.monitoring_notification_channels), 0) + ? rule.value.disable_default_iam_recipients + : null + ) + schema_version = "1.0" + } + } +} diff --git a/modules/billing-account/iam.tf b/modules/billing-account/iam.tf new file mode 100644 index 0000000000..d8cec25813 --- /dev/null +++ b/modules/billing-account/iam.tf @@ -0,0 +1,70 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description IAM bindings. + +locals { + _group_iam_roles = distinct(flatten(values(var.group_iam))) + _group_iam = { + for r in local._group_iam_roles : r => [ + for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local._group_iam))) : + role => concat( + try(var.iam[role], []), + try(local._group_iam[role], []) + ) + } +} + +resource "google_billing_account_iam_binding" "authoritative" { + for_each = local.iam + billing_account_id = var.id + role = each.key + members = each.value +} + +resource "google_billing_account_iam_binding" "bindings" { + for_each = var.iam_bindings + billing_account_id = var.id + role = each.value.role + members = each.value.members + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_billing_account_iam_member" "bindings" { + for_each = var.iam_bindings_additive + billing_account_id = var.id + role = each.value.role + member = each.value.member + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} diff --git a/modules/billing-account/logging.tf b/modules/billing-account/logging.tf new file mode 100644 index 0000000000..9c96ea93f2 --- /dev/null +++ b/modules/billing-account/logging.tf @@ -0,0 +1,92 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Log sinks and supporting resources. + +locals { + sink_bindings = { + for type in ["bigquery", "pubsub", "logging", "storage"] : + type => { + for name, sink in var.logging_sinks : + name => sink + if sink.type == type + } + } +} + +resource "google_logging_billing_account_sink" "sink" { + for_each = var.logging_sinks + name = each.key + description = coalesce(each.value.description, "${each.key} (Terraform-managed).") + billing_account = var.id + destination = "${each.value.type}.googleapis.com/${each.value.destination}" + filter = each.value.filter + disabled = each.value.disabled + + dynamic "bigquery_options" { + for_each = each.value.type == "biquery" && each.value.bq_partitioned_table != false ? [""] : [] + content { + use_partitioned_tables = each.value.bq_partitioned_table + } + } + + dynamic "exclusions" { + for_each = each.value.exclusions + iterator = exclusion + content { + name = exclusion.key + filter = exclusion.value.filter + description = exclusion.value.description + disabled = exclusion.value.disabled + } + } +} + +resource "google_storage_bucket_iam_member" "gcs-sinks-binding" { + for_each = local.sink_bindings["storage"] + bucket = each.value.destination + role = "roles/storage.objectCreator" + member = google_logging_billing_account_sink.sink[each.key].writer_identity +} + +resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" { + for_each = local.sink_bindings["bigquery"] + project = split("/", each.value.destination)[1] + dataset_id = split("/", each.value.destination)[3] + role = "roles/bigquery.dataEditor" + member = google_logging_billing_account_sink.sink[each.key].writer_identity +} + +resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" { + for_each = local.sink_bindings["pubsub"] + project = split("/", each.value.destination)[1] + topic = split("/", each.value.destination)[3] + role = "roles/pubsub.publisher" + member = google_logging_billing_account_sink.sink[each.key].writer_identity +} + +resource "google_project_iam_member" "bucket-sinks-binding" { + for_each = local.sink_bindings["logging"] + project = split("/", each.value.destination)[1] + role = "roles/logging.bucketWriter" + member = google_logging_billing_account_sink.sink[each.key].writer_identity + + condition { + title = "${each.key} bucket writer" + description = "Grants bucketWriter to ${google_logging_billing_account_sink.sink[each.key].writer_identity} used by log sink ${each.key} on billing account ${var.id}" + expression = "resource.name.endsWith('${each.value.destination}')" + } +} diff --git a/modules/billing-account/main.tf b/modules/billing-account/main.tf new file mode 100644 index 0000000000..f477cbb423 --- /dev/null +++ b/modules/billing-account/main.tf @@ -0,0 +1,21 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_billing_project_info" "default" { + for_each = toset(var.projects) + billing_account = var.id + project = each.key +} diff --git a/modules/billing-account/outputs.tf b/modules/billing-account/outputs.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/billing-account/variables.tf b/modules/billing-account/variables.tf new file mode 100644 index 0000000000..18a8b419a1 --- /dev/null +++ b/modules/billing-account/variables.tf @@ -0,0 +1,198 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "budget_notification_channels" { + description = "Notification channels used by budget alerts." + type = map(object({ + project_id = string + type = string + description = optional(string) + display_name = optional(string) + enabled = optional(bool, true) + force_delete = optional(bool) + labels = optional(map(string)) + sensitive_labels = optional(list(object({ + auth_token = optional(string) + password = optional(string) + service_key = optional(string) + }))) + user_labels = optional(map(string)) + })) + nullable = false + default = {} + validation { + condition = alltrue([ + for k, v in var.budget_notification_channels : contains([ + "campfire", "email", "google_chat", "hipchat", "pagerduty", + "pubsub", "slack", "sms", "webhook_basicauth", "webhook_tokenauth" + ], v.type) + ]) + error_message = "Invalid notification channel type." + } +} + +variable "budgets" { + description = "Billing budgets. Notification channels are either keys in corresponding variable, or external ids." + type = map(object({ + amount = object({ + currency_code = optional(string) + nanos = optional(number) + units = optional(number) + use_last_period = optional(bool) + }) + display_name = optional(string) + # TODO: check that only one filter is supported + filter = optional(object({ + credit_types_treatment = optional(object({ + exclude_all = optional(bool) + include_specified = optional(list(string)) + })) + label = optional(object({ + key = string + value = string + })) + # TODO: check that period is optional + period = optional(object({ + calendar = optional(string) + custom = optional(object({ + start_date = object({ + day = number + month = number + year = number + }) + end_date = optional(object({ + day = number + month = number + year = number + })) + })) + })) + projects = optional(list(string), []) + resource_ancestors = optional(list(string), []) + services = optional(list(string), []) + subaccounts = optional(list(string), []) + })) + threshold_rules = optional(map(object({ + percent = number + forecasted_spend = optional(bool) + })), {}) + all_update_rules = optional(map(object({ + disable_default_iam_recipients = optional(bool) + monitoring_notification_channels = optional(list(string)) + pubsub_topic = optional(string) + })), {}) + })) + nullable = false + default = {} + validation { + condition = alltrue([ + for k, v in var.budgets : v.amount != null && ( + try(v.amount.use_last_period, null) == true || + try(v.amount.units, null) != null + ) + ]) + error_message = "Each budgets needs to have amount units specified, or use last period." + } +} + +variable "group_iam" { + description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_bindings" { + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." + type = map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + nullable = false + default = {} +} + +variable "iam_bindings_additive" { + description = "Individual additive IAM bindings. Keys are arbitrary." + type = map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + nullable = false + default = {} +} + +variable "id" { + description = "Billing account id." + type = string +} + +variable "logging_sinks" { + description = "Logging sinks to create for the organization." + type = map(object({ + destination = string + type = string + bq_partitioned_table = optional(bool) + description = optional(string) + disabled = optional(bool, false) + exclusions = optional(map(object({ + filter = string + description = optional(string) + disabled = optional(bool) + })), {}) + filter = optional(string) + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } + validation { + condition = alltrue([ + for k, v in var.logging_sinks : + v.bq_partitioned_table != true || v.type == "bigquery" + ]) + error_message = "Can only set bq_partitioned_table when type is `bigquery`." + } +} + +variable "projects" { + description = "Projects associated with this billing account." + type = list(string) + nullable = false + default = [] +} diff --git a/modules/billing-account/versions.tf b/modules/billing-account/versions.tf new file mode 100644 index 0000000000..3963660f08 --- /dev/null +++ b/modules/billing-account/versions.tf @@ -0,0 +1,29 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.4.4" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 5.0.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 5.0.0" # tftest + } + } +} + + diff --git a/modules/folder/iam.tf b/modules/folder/iam.tf index 20025b2831..56ed62126e 100644 --- a/modules/folder/iam.tf +++ b/modules/folder/iam.tf @@ -14,7 +14,7 @@ * limitations under the License. */ -# tfdoc:file:description IAM bindings, roles and audit logging resources. +# tfdoc:file:description IAM bindings. locals { _group_iam_roles = distinct(flatten(values(var.group_iam))) diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 86efc21546..1c55168187 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -109,7 +109,7 @@ variable "logging_exclusions" { } variable "logging_sinks" { - description = "Logging sinks to create for the organization." + description = "Logging sinks to create for the folder." type = map(object({ bq_partitioned_table = optional(bool) description = optional(string) From 77d43ed040d16b1a3715f28f693fbd19bb10bb08 Mon Sep 17 00:00:00 2001 From: Ludo Date: Sat, 14 Oct 2023 14:26:08 +0200 Subject: [PATCH 2/6] readme and tests --- README.md | 2 +- modules/README.md | 14 +- modules/billing-account/README.md | 237 ++++++++++++++++++ modules/billing-account/budgets.tf | 92 ++++--- modules/billing-account/outputs.tf | 31 +++ modules/billing-account/variables.tf | 28 ++- modules/billing-account/versions.tf | 2 - modules/billing-budget/README.md | 89 ------- modules/billing-budget/main.tf | 95 ------- modules/billing-budget/outputs.tf | 25 -- modules/billing-budget/variables.tf | 95 ------- modules/billing-budget/versions.tf | 29 --- .../examples/budget-monitoring-channel.yaml | 60 +++++ .../examples/budget-pubsub.yaml | 56 +++++ .../examples/budget-simple.yaml | 44 ++++ .../modules/billing_account/examples/iam.yaml | 45 ++++ .../billing_account/examples/logging.yaml | 43 ++++ 17 files changed, 597 insertions(+), 390 deletions(-) create mode 100644 modules/billing-account/README.md delete mode 100644 modules/billing-budget/README.md delete mode 100644 modules/billing-budget/main.tf delete mode 100644 modules/billing-budget/outputs.tf delete mode 100644 modules/billing-budget/variables.tf delete mode 100644 modules/billing-budget/versions.tf create mode 100644 tests/modules/billing_account/examples/budget-monitoring-channel.yaml create mode 100644 tests/modules/billing_account/examples/budget-pubsub.yaml create mode 100644 tests/modules/billing_account/examples/budget-simple.yaml create mode 100644 tests/modules/billing_account/examples/iam.yaml create mode 100644 tests/modules/billing_account/examples/logging.yaml diff --git a/README.md b/README.md index 84dbdca20c..08907864fb 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The current list of modules supports most of the core foundational and networkin Currently available modules: -- **foundational** - [billing budget](./modules/billing-budget), [Cloud Identity group](./modules/cloud-identity-group/), [folder](./modules/folder), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [organization](./modules/organization), [project](./modules/project), [projects-data-source](./modules/projects-data-source) +- **foundational** - [billing account](./modules/billing-account), [Cloud Identity group](./modules/cloud-identity-group/), [folder](./modules/folder), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [organization](./modules/organization), [project](./modules/project), [projects-data-source](./modules/projects-data-source) - **networking** - [DNS](./modules/dns), [DNS Response Policy](./modules/dns-response-policy/), [Cloud Endpoints](./modules/endpoints), [address reservation](./modules/net-address), [NAT](./modules/net-cloudnat), [VLAN Attachment](./modules/net-vlan-attachment/), [External Application LB](./modules/net-lb-app-ext/), [External Passthrough Network LB](./modules/net-lb-ext), [Firewall policy](./modules/net-firewall-policy), [Internal Application LB](./modules/net-lb-app-int), [Internal Passthrough Network LB](./modules/net-lb-int), [Internal Proxy Network LB](./modules/net-lb-proxy-int), [IPSec over Interconnect](./modules/net-ipsec-over-interconnect), [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [VPN static](./modules/net-vpn-static), [Service Directory](./modules/service-directory), [Secure Web Proxy](./modules/net-swp) - **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster-standard), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool), [GCVE private cloud](./modules/gcve-private-cloud) - **data** - [BigQuery dataset](./modules/bigquery-dataset), [Bigtable instance](./modules/bigtable-instance), [Dataplex](./modules/dataplex), [Dataplex DataScan](./modules/dataplex-datascan/), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Datafusion](./modules/datafusion), [Dataproc](./modules/dataproc), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub) diff --git a/modules/README.md b/modules/README.md index 44df93bebb..4fbbd140e9 100644 --- a/modules/README.md +++ b/modules/README.md @@ -30,14 +30,14 @@ These modules are used in the examples included in this repository. If you are u ## Foundational modules -- [billing budget](./billing-budget) +- [Billing account](./billing-account) - [Cloud Identity group](./cloud-identity-group/) -- [folder](./folder) -- [service accounts](./iam-service-account) -- [logging bucket](./logging-bucket) -- [organization](./organization) -- [project](./project) -- [projects-data-source](./projects-data-source) +- [Folder](./folder) +- [Service accounts](./iam-service-account) +- [Logging bucket](./logging-bucket) +- [Organization](./organization) +- [Project](./project) +- [Projects (data source)](./projects-data-source) ## Networking modules diff --git a/modules/billing-account/README.md b/modules/billing-account/README.md new file mode 100644 index 0000000000..f5fd4e2f18 --- /dev/null +++ b/modules/billing-account/README.md @@ -0,0 +1,237 @@ +# Billing Account Module + +This module allows managing resources and policies related to a billing account: + +- IAM bindings +- log sinks +- billing budgets and their notifications + +Managing billing-related resources via application default credentials [requires a billing project to be set](https://cloud.google.com/docs/authentication/troubleshoot-adc#user-creds-client-based). To configure one via Terraform you can use a snippet similar to this one: + +```hcl +provider "google" { + billing_project = "my-project" + user_project_override = true +} +# tftest skip +``` + + +- [Examples](#examples) + - [IAM bindings](#iam-bindings) + - [Log sinks](#log-sinks) + - [Billing budgets](#billing-budgets) + - [PubSub update rules](#pubsub-update-rules) + - [Monitoring channels](#monitoring-channels) +- [Variables](#variables) +- [Outputs](#outputs) + + +## Examples + +### IAM bindings + +Billing account IAM bindings implement [the same interface](../__docs/20230816-iam-refactor.md) used for all other modules. + +```hcl +module "billing-account" { + source = "./fabric/modules/billing-account" + id = "012345-ABCDEF-012345" + group_iam = { + "billing-admins@example.org" = ["roles/billing.admin"] + } + iam = { + "roles/billing.admin" = [ + "serviceAccount:foo@myprj.iam.gserviceaccount.com" + ] + } + iam_bindings = { + conditional-admin = { + members = [ + "serviceAccount:pf-dev@myprj.iam.gserviceaccount.com" + ] + role = "roles/billing.admin" + condition = { + title = "pf-dev-conditional-billing-admin" + expression = ( + "resource.matchTag('123456/environment', 'development')" + ) + } + } + } + iam_bindings_additive = { + sa-net-iac-user = { + member = "serviceAccount:net-iac-0@myprj.iam.gserviceaccount.com" + role = "roles/billing.user" + } + } +} +# tftest modules=1 resources=3 inventory=iam.yaml +``` + +### Log sinks + +Billing account log sinks use the same format used for log sinks in the resource manager modules (organization, folder, project). + +```hcl +module "log-bucket-all" { + source = "./fabric/modules/logging-bucket" + parent_type = "project" + parent = "myprj" + id = "billing-account-all" +} + +module "billing-account" { + source = "./fabric/modules/billing-account" + id = "012345-ABCDEF-012345" + logging_sinks = { + all = { + destination = module.log-bucket-all.id + type = "logging" + } + } +} +# tftest modules=2 resources=3 inventory=logging.yaml +``` + +### Billing budgets + +Billing budgets expose all the attributes of the underlying resource, and allow using external notification channels, or creating them via this same module. + +```hcl +module "billing-account" { + source = "./fabric/modules/billing-account" + id = "012345-ABCDEF-012345" + budgets = { + folder-net-month-current-100 = { + display_name = "100 dollars in current spend" + amount = { + units = 100 + } + filter = { + period = { + calendar = "MONTH" + } + resource_ancestors = ["folders/1234567890"] + } + threshold_rules = [ + { percent = 0.5 }, + { percent = 0.75 } + ] + } + } +} +# tftest modules=1 resources=1 inventory=budget-simple.yaml +``` + +#### PubSub update rules + +Update rules can notify pubsub topics. + +```hcl +module "pubsub-billing-topic" { + source = "./fabric/modules/pubsub" + project_id = "my-prj" + name = "budget-default" +} + +module "billing-account" { + source = "./fabric/modules/billing-account" + id = "012345-ABCDEF-012345" + budgets = { + folder-net-month-current-100 = { + display_name = "100 dollars in current spend" + amount = { + units = 100 + } + filter = { + period = { + calendar = "MONTH" + } + resource_ancestors = ["folders/1234567890"] + } + threshold_rules = [ + { percent = 0.5 }, + { percent = 0.75 } + ] + update_rules = { + default = { + pubsub_topic = module.pubsub-billing-topic.id + } + } + } + } +} +# tftest modules=2 resources=2 inventory=budget-pubsub.yaml +``` + +#### Monitoring channels + +Monitoring channels can be referenced in update rules either by passing in an existing channel id, or by using a reference to a key in the `budget_notification_channels` variable, that allows managing ad hoc monitoring channels. + + + +```hcl +module "billing-account" { + source = "./fabric/modules/billing-account" + id = "012345-ABCDEF-012345" + budget_notification_channels = { + billing-default = { + project_id = "tf-playground-simple" + type = "email" + labels = { + email_address = "gcp-billing-admins@example.com" + } + } + } + budgets = { + folder-net-month-current-100 = { + display_name = "100 dollars in current spend" + amount = { + units = 100 + } + filter = { + period = { + calendar = "MONTH" + } + resource_ancestors = ["folders/1234567890"] + } + threshold_rules = [ + { percent = 0.5 }, + { percent = 0.75 } + ] + update_rules = { + default = { + disable_default_iam_recipients = true + monitoring_notification_channels = ["billing-default"] + } + } + } + } +} +# tftest modules=1 resources=2 inventory=budget-monitoring-channel.yaml +``` + + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [id](variables.tf#L165) | Billing account id. | string | ✓ | | +| [budget_notification_channels](variables.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} | +| [budgets](variables.tf#L47) | Billing budgets. Notification channels are either keys in corresponding variable, or external ids. | map(object({…})) | | {} | +| [group_iam](variables.tf#L121) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | +| [iam](variables.tf#L128) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L135) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L150) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [logging_sinks](variables.tf#L170) | Logging sinks to create for the organization. | map(object({…})) | | {} | +| [projects](variables.tf#L203) | Projects associated with this billing account. | list(string) | | [] | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [billing_budget_ids](outputs.tf#L17) | Billing budget ids. | | +| [monitoring_notification_channel_ids](outputs.tf#L25) | Monitoring notification channel ids. | | + diff --git a/modules/billing-account/budgets.tf b/modules/billing-account/budgets.tf index 9ef7a2c9eb..d3b695ed69 100644 --- a/modules/billing-account/budgets.tf +++ b/modules/billing-account/budgets.tf @@ -25,11 +25,11 @@ resource "google_monitoring_notification_channel" "default" { project = each.value.project_id enabled = each.value.enabled force_delete = each.value.force_delete - type = each.value.tupe + type = each.value.type labels = each.value.labels user_labels = each.value.user_labels dynamic "sensitive_labels" { - for_each = toset(coalesce(each.value.sensitive.labels, {})) + for_each = toset(coalesce(each.value.sensitive_labels, [])) content { auth_token = sensitive_labels.value.auth_token password = sensitive_labels.value.password @@ -51,78 +51,94 @@ resource "google_billing_budget" "default" { dynamic "amount" { for_each = each.value.amount.use_last_period != true ? [""] : [] content { - currency_code = each.value.amount.currency_code - nanos = each.value.amount.nanos - units = each.value.amount.units + specified_amount { + currency_code = each.value.amount.currency_code + nanos = each.value.amount.nanos + units = each.value.amount.units + } } } budget_filter { - calendar_period = try(each.value.period.calendar, null) + calendar_period = try(each.value.filter.period.calendar, null) credit_types_treatment = ( - try(each.value.credit_types_treatment.exclude_all, null) == true + try(each.value.filter.credit_types_treatment.exclude_all, null) == true ? "EXCLUDE_ALL_CREDITS" : ( - try(each.value.credit_types_treatment.include_specified, null) != null + try(each.value.filter.credit_types_treatment.include_specified, null) != null ? "INCLUDE_SPECIFIED_CREDITS" : "INCLUDE_ALL_CREDITS" ) ) - labels = each.value.label == null ? null : { - (each.value.label.key) = each.value.label.value + labels = each.value.filter.label == null ? null : { + (each.value.filter.label.key) = each.value.filter.label.value } - projects = each.value.projects - resource_ancestors = each.value.resource_ancestors - services = each.value.services - subaccounts = each.value.subaccounts + projects = ( + each.value.filter.projects == null + ? null + : each.value.filter.projects + ) + resource_ancestors = ( + each.value.filter.resource_ancestors == null + ? null + : each.value.filter.resource_ancestors + ) + services = ( + each.value.filter.services == null + ? null + : each.value.filter.services + ) + subaccounts = ( + each.value.filter.subaccounts == null + ? null + : each.value.filter.subaccounts + ) dynamic "custom_period" { - for_each = try(each.value.period.custom, null) != null ? [""] : [] + for_each = try(each.value.filter.period.custom, null) != null ? [""] : [] content { start_date { - day = each.value.period.custom.start_date.day - month = each.value.period.custom.start_date.month - year = each.value.period.custom.start_date.year + day = each.value.filter.period.custom.start_date.day + month = each.value.filter.period.custom.start_date.month + year = each.value.filter.period.custom.start_date.year } dynamic "end_date" { - for_each = try(each.value.period.custom.end_date, null) != null ? [""] : [] + for_each = try(each.value.filter.period.custom.end_date, null) != null ? [""] : [] content { - day = each.value.period.custom.end_date.day - month = each.value.period.custom.end_date.month - year = each.value.period.custom.end_date.year + day = each.value.filter.period.custom.end_date.day + month = each.value.filter.period.custom.end_date.month + year = each.value.filter.period.custom.end_date.year } } } } } dynamic "threshold_rules" { - for_each = each.value.threshold_rules + for_each = toset(each.value.threshold_rules) iterator = rule content { threshold_percent = rule.value.percent spend_basis = ( - rule.value.forecasted_spend + rule.value.forecasted_spend == true ? "FORECASTED_SPEND" : "CURRENT_SPEND" ) } } dynamic "all_updates_rule" { - for_each = each.value.all_update_rules + for_each = each.value.update_rules iterator = rule content { - monitoring_notification_channels = [ - for v in rule.value.monitoring_notification_channels : try( - google_monitoring_notification_channel[v].id, v - ) - ] - pubsub_topic = var.pubsub_topic - # disable_default_iam_recipients can only be set if - # monitoring_notification_channels is nonempty - disable_default_iam_recipients = ( - try(length(rule.value.monitoring_notification_channels), 0) - ? rule.value.disable_default_iam_recipients - : null + pubsub_topic = rule.value.pubsub_topic + schema_version = "1.0" + disable_default_iam_recipients = rule.value.disable_default_iam_recipients + monitoring_notification_channels = ( + rule.value.monitoring_notification_channels == null + ? null + : [ + for v in rule.value.monitoring_notification_channels : try( + google_monitoring_notification_channel.default[v].id, v + ) + ] ) - schema_version = "1.0" } } } diff --git a/modules/billing-account/outputs.tf b/modules/billing-account/outputs.tf index e69de29bb2..f8359c530e 100644 --- a/modules/billing-account/outputs.tf +++ b/modules/billing-account/outputs.tf @@ -0,0 +1,31 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "billing_budget_ids" { + description = "Billing budget ids." + value = { + for k, v in google_billing_budget.default : + k => v.id + } +} + +output "monitoring_notification_channel_ids" { + description = "Monitoring notification channel ids." + value = { + for k, v in google_monitoring_notification_channel.default : + k => v.id + } +} diff --git a/modules/billing-account/variables.tf b/modules/billing-account/variables.tf index 18a8b419a1..61f30005c3 100644 --- a/modules/billing-account/variables.tf +++ b/modules/billing-account/variables.tf @@ -54,7 +54,6 @@ variable "budgets" { use_last_period = optional(bool) }) display_name = optional(string) - # TODO: check that only one filter is supported filter = optional(object({ credit_types_treatment = optional(object({ exclude_all = optional(bool) @@ -64,7 +63,6 @@ variable "budgets" { key = string value = string })) - # TODO: check that period is optional period = optional(object({ calendar = optional(string) custom = optional(object({ @@ -80,16 +78,16 @@ variable "budgets" { })) })) })) - projects = optional(list(string), []) - resource_ancestors = optional(list(string), []) - services = optional(list(string), []) - subaccounts = optional(list(string), []) + projects = optional(list(string)) + resource_ancestors = optional(list(string)) + services = optional(list(string)) + subaccounts = optional(list(string)) })) - threshold_rules = optional(map(object({ + threshold_rules = optional(list(object({ percent = number forecasted_spend = optional(bool) - })), {}) - all_update_rules = optional(map(object({ + })), []) + update_rules = optional(map(object({ disable_default_iam_recipients = optional(bool) monitoring_notification_channels = optional(list(string)) pubsub_topic = optional(string) @@ -106,6 +104,18 @@ variable "budgets" { ]) error_message = "Each budgets needs to have amount units specified, or use last period." } + validation { + condition = alltrue(flatten([ + for k, v in var.budgets : [ + for kk, vv in v.update_rules : [ + vv.monitoring_notification_channels != null + || + vv.pubsub_topic != null + ] + ] + ])) + error_message = "Budget notification rules need either a pubsub topic or monitoring channels defined." + } } variable "group_iam" { diff --git a/modules/billing-account/versions.tf b/modules/billing-account/versions.tf index 3963660f08..3adb6d4488 100644 --- a/modules/billing-account/versions.tf +++ b/modules/billing-account/versions.tf @@ -25,5 +25,3 @@ terraform { } } } - - diff --git a/modules/billing-budget/README.md b/modules/billing-budget/README.md deleted file mode 100644 index 72fe574b5c..0000000000 --- a/modules/billing-budget/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Google Cloud Billing Budget Module - -This module allows creating a Cloud Billing budget for a set of services and projects. - -To create billing budgets you need one of the following IAM roles on the target billing account: - -* Billing Account Administrator -* Billing Account Costs Manager - -## Examples - -### Simple email notification - -Send a notification to an email when a set of projects reach $100 of spend. - -```hcl -module "budget" { - source = "./fabric/modules/billing-budget" - billing_account = var.billing_account_id - name = "$100 budget" - amount = 100 - thresholds = { - current = [0.5, 0.75, 1.0] - forecasted = [1.0] - } - projects = [ - "projects/123456789000", - "projects/123456789111" - ] - email_recipients = { - project_id = "my-project" - emails = ["user@example.com"] - } -} -# tftest modules=1 resources=2 inventory=email.yaml -``` - -### Pubsub notification - -Send a notification to a PubSub topic the total spend of a billing account reaches the previous month's spend. - - -```hcl -module "budget" { - source = "./fabric/modules/billing-budget" - billing_account = var.billing_account_id - name = "previous period budget" - amount = 0 - thresholds = { - current = [1.0] - forecasted = [] - } - pubsub_topic = module.pubsub.id -} - -module "pubsub" { - source = "./fabric/modules/pubsub" - project_id = var.project_id - name = "budget-topic" -} - -# tftest modules=2 resources=2 inventory=pubsub.yaml -``` - - -## Variables - -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [billing_account](variables.tf#L23) | Billing account id. | string | ✓ | | -| [name](variables.tf#L50) | Budget name. | string | ✓ | | -| [thresholds](variables.tf#L85) | Thresholds percentages at which alerts are sent. Must be a value between 0 and 1. | object({…}) | ✓ | | -| [amount](variables.tf#L17) | Amount in the billing account's currency for the budget. Use 0 to set budget to 100% of last period's spend. | number | | 0 | -| [credit_treatment](variables.tf#L28) | How credits should be treated when determining spend for threshold calculations. Only INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS are supported. | string | | "INCLUDE_ALL_CREDITS" | -| [email_recipients](variables.tf#L41) | Emails where budget notifications will be sent. Setting this will create a notification channel for each email in the specified project. | object({…}) | | null | -| [notification_channels](variables.tf#L55) | Monitoring notification channels where to send updates. | list(string) | | null | -| [notify_default_recipients](variables.tf#L61) | Notify Billing Account Administrators and Billing Account Users IAM roles for the target account. | bool | | false | -| [projects](variables.tf#L67) | List of projects of the form projects/{project_number}, specifying that usage from only this set of projects should be included in the budget. Set to null to include all projects linked to the billing account. | list(string) | | null | -| [pubsub_topic](variables.tf#L73) | The ID of the Cloud Pub/Sub topic where budget related messages will be published. | string | | null | -| [services](variables.tf#L79) | List of services of the form services/{service_id}, specifying that usage from only this set of services should be included in the budget. Set to null to include usage for all services. | list(string) | | null | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| [budget](outputs.tf#L17) | Budget resource. | | -| [id](outputs.tf#L22) | Fully qualified budget id. | | - - diff --git a/modules/billing-budget/main.tf b/modules/billing-budget/main.tf deleted file mode 100644 index 2c6838dc7f..0000000000 --- a/modules/billing-budget/main.tf +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -locals { - spend_basis = { - current = "CURRENT_SPEND" - forecasted = "FORECASTED_SPEND" - } - threshold_pairs = flatten([ - for type, values in var.thresholds : [ - for value in values : { - spend_basis = local.spend_basis[type] - threshold_percent = value - } - ] - ]) - - notification_channels = concat( - [for channel in google_monitoring_notification_channel.email_channels : channel.id], - coalesce(var.notification_channels, []) - ) -} - -resource "google_monitoring_notification_channel" "email_channels" { - for_each = toset(try(var.email_recipients.emails, [])) - display_name = "${var.name} budget email notification (${each.value})" - type = "email" - project = var.email_recipients.project_id - labels = { - email_address = each.value - } - user_labels = {} -} - - -resource "google_billing_budget" "budget" { - billing_account = var.billing_account - display_name = var.name - - budget_filter { - projects = var.projects - credit_types_treatment = var.credit_treatment - services = var.services - } - - dynamic "amount" { - for_each = var.amount == 0 ? [1] : [] - content { - last_period_amount = true - } - } - - dynamic "amount" { - for_each = var.amount != 0 ? [1] : [] - content { - dynamic "specified_amount" { - for_each = var.amount != 0 ? [1] : [] - content { - units = var.amount - } - } - } - } - - dynamic "threshold_rules" { - for_each = local.threshold_pairs - iterator = threshold - content { - threshold_percent = threshold.value.threshold_percent - spend_basis = threshold.value.spend_basis - } - } - - all_updates_rule { - monitoring_notification_channels = local.notification_channels - pubsub_topic = var.pubsub_topic - # disable_default_iam_recipients can only be set if - # monitoring_notification_channels is nonempty - disable_default_iam_recipients = try(length(var.notification_channels), 0) > 0 && !var.notify_default_recipients - schema_version = "1.0" - } -} diff --git a/modules/billing-budget/outputs.tf b/modules/billing-budget/outputs.tf deleted file mode 100644 index 530f857381..0000000000 --- a/modules/billing-budget/outputs.tf +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -output "budget" { - description = "Budget resource." - value = google_billing_budget.budget -} - -output "id" { - description = "Fully qualified budget id." - value = google_billing_budget.budget.id -} diff --git a/modules/billing-budget/variables.tf b/modules/billing-budget/variables.tf deleted file mode 100644 index 003f928e51..0000000000 --- a/modules/billing-budget/variables.tf +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "amount" { - description = "Amount in the billing account's currency for the budget. Use 0 to set budget to 100% of last period's spend." - type = number - default = 0 -} - -variable "billing_account" { - description = "Billing account id." - type = string -} - -variable "credit_treatment" { - description = "How credits should be treated when determining spend for threshold calculations. Only INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS are supported." - type = string - default = "INCLUDE_ALL_CREDITS" - validation { - condition = ( - var.credit_treatment == "INCLUDE_ALL_CREDITS" || - var.credit_treatment == "EXCLUDE_ALL_CREDITS" - ) - error_message = "Argument credit_treatment must be INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS." - } -} - -variable "email_recipients" { - description = "Emails where budget notifications will be sent. Setting this will create a notification channel for each email in the specified project." - type = object({ - project_id = string - emails = list(string) - }) - default = null -} - -variable "name" { - description = "Budget name." - type = string -} - -variable "notification_channels" { - description = "Monitoring notification channels where to send updates." - type = list(string) - default = null -} - -variable "notify_default_recipients" { - description = "Notify Billing Account Administrators and Billing Account Users IAM roles for the target account." - type = bool - default = false -} - -variable "projects" { - description = "List of projects of the form projects/{project_number}, specifying that usage from only this set of projects should be included in the budget. Set to null to include all projects linked to the billing account." - type = list(string) - default = null -} - -variable "pubsub_topic" { - description = "The ID of the Cloud Pub/Sub topic where budget related messages will be published." - type = string - default = null -} - -variable "services" { - description = "List of services of the form services/{service_id}, specifying that usage from only this set of services should be included in the budget. Set to null to include usage for all services." - type = list(string) - default = null -} - -variable "thresholds" { - description = "Thresholds percentages at which alerts are sent. Must be a value between 0 and 1." - type = object({ - current = list(number) - forecasted = list(number) - }) - validation { - condition = length(var.thresholds.current) > 0 || length(var.thresholds.forecasted) > 0 - error_message = "Must specify at least one budget threshold." - } -} diff --git a/modules/billing-budget/versions.tf b/modules/billing-budget/versions.tf deleted file mode 100644 index 3963660f08..0000000000 --- a/modules/billing-budget/versions.tf +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -terraform { - required_version = ">= 1.4.4" - required_providers { - google = { - source = "hashicorp/google" - version = ">= 5.0.0" # tftest - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 5.0.0" # tftest - } - } -} - - diff --git a/tests/modules/billing_account/examples/budget-monitoring-channel.yaml b/tests/modules/billing_account/examples/budget-monitoring-channel.yaml new file mode 100644 index 0000000000..3909336c93 --- /dev/null +++ b/tests/modules/billing_account/examples/budget-monitoring-channel.yaml @@ -0,0 +1,60 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.billing-account.google_billing_budget.default["folder-net-month-current-100"]: + all_updates_rule: + - disable_default_iam_recipients: true + pubsub_topic: null + schema_version: '1.0' + amount: + - last_period_amount: null + specified_amount: + - nanos: null + units: '100' + billing_account: 012345-ABCDEF-012345 + budget_filter: + - calendar_period: null + credit_types_treatment: INCLUDE_ALL_CREDITS + custom_period: [] + projects: null + resource_ancestors: + - folders/1234567890 + display_name: 100 dollars in current spend + threshold_rules: + - spend_basis: CURRENT_SPEND + threshold_percent: 0.5 + - spend_basis: CURRENT_SPEND + threshold_percent: 0.75 + timeouts: null + module.billing-account.google_monitoring_notification_channel.default["billing-default"]: + description: null + display_name: Budget email notification billing-default. + enabled: true + force_delete: false + labels: + email_address: gcp-billing-admins@example.com + project: tf-playground-simple + sensitive_labels: [] + timeouts: null + type: email + user_labels: null + +counts: + google_billing_budget: 1 + google_monitoring_notification_channel: 1 + modules: 1 + resources: 2 + +outputs: {} diff --git a/tests/modules/billing_account/examples/budget-pubsub.yaml b/tests/modules/billing_account/examples/budget-pubsub.yaml new file mode 100644 index 0000000000..76c1f55914 --- /dev/null +++ b/tests/modules/billing_account/examples/budget-pubsub.yaml @@ -0,0 +1,56 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.billing-account.google_billing_budget.default["folder-net-month-current-100"]: + all_updates_rule: + - disable_default_iam_recipients: false + monitoring_notification_channels: null + pubsub_topic: projects/my-prj/topics/budget-default + schema_version: '1.0' + amount: + - last_period_amount: null + specified_amount: + - nanos: null + units: '100' + billing_account: 012345-ABCDEF-012345 + budget_filter: + - calendar_period: null + credit_types_treatment: INCLUDE_ALL_CREDITS + custom_period: [] + projects: null + resource_ancestors: + - folders/1234567890 + display_name: 100 dollars in current spend + threshold_rules: + - spend_basis: CURRENT_SPEND + threshold_percent: 0.5 + - spend_basis: CURRENT_SPEND + threshold_percent: 0.75 + timeouts: null + module.pubsub-billing-topic.google_pubsub_topic.default: + kms_key_name: null + labels: null + message_retention_duration: null + name: budget-default + project: my-prj + timeouts: null + +counts: + google_billing_budget: 1 + google_pubsub_topic: 1 + modules: 2 + resources: 2 + +outputs: {} diff --git a/tests/modules/billing_account/examples/budget-simple.yaml b/tests/modules/billing_account/examples/budget-simple.yaml new file mode 100644 index 0000000000..8fd87441db --- /dev/null +++ b/tests/modules/billing_account/examples/budget-simple.yaml @@ -0,0 +1,44 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.billing-account.google_billing_budget.default["folder-net-month-current-100"]: + all_updates_rule: [] + amount: + - last_period_amount: null + specified_amount: + - nanos: null + units: '100' + billing_account: 012345-ABCDEF-012345 + budget_filter: + - calendar_period: null + credit_types_treatment: INCLUDE_ALL_CREDITS + custom_period: [] + projects: null + resource_ancestors: + - folders/1234567890 + display_name: 100 dollars in current spend + threshold_rules: + - spend_basis: CURRENT_SPEND + threshold_percent: 0.5 + - spend_basis: CURRENT_SPEND + threshold_percent: 0.75 + timeouts: null + +counts: + google_billing_budget: 1 + modules: 1 + resources: 1 + +outputs: {} diff --git a/tests/modules/billing_account/examples/iam.yaml b/tests/modules/billing_account/examples/iam.yaml new file mode 100644 index 0000000000..1cac36d583 --- /dev/null +++ b/tests/modules/billing_account/examples/iam.yaml @@ -0,0 +1,45 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.billing-account.google_billing_account_iam_binding.authoritative["roles/billing.admin"]: + billing_account_id: 012345-ABCDEF-012345 + condition: [] + members: + - group:billing-admins@example.org + - serviceAccount:foo@myprj.iam.gserviceaccount.com + role: roles/billing.admin + module.billing-account.google_billing_account_iam_binding.bindings["conditional-admin"]: + billing_account_id: 012345-ABCDEF-012345 + condition: + - description: null + expression: resource.matchTag('123456/environment', 'development') + title: pf-dev-conditional-billing-admin + members: + - serviceAccount:pf-dev@myprj.iam.gserviceaccount.com + role: roles/billing.admin + module.billing-account.google_billing_account_iam_member.bindings["sa-net-iac-user"]: + billing_account_id: 012345-ABCDEF-012345 + condition: [] + member: serviceAccount:net-iac-0@myprj.iam.gserviceaccount.com + role: roles/billing.user + +counts: + google_billing_account_iam_binding: 2 + google_billing_account_iam_member: 1 + modules: 1 + resources: 3 + +outputs: {} + diff --git a/tests/modules/billing_account/examples/logging.yaml b/tests/modules/billing_account/examples/logging.yaml new file mode 100644 index 0000000000..4496f01ac4 --- /dev/null +++ b/tests/modules/billing_account/examples/logging.yaml @@ -0,0 +1,43 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.billing-account.google_logging_billing_account_sink.sink["all"]: + billing_account: 012345-ABCDEF-012345 + description: all (Terraform-managed). + disabled: false + exclusions: [] + filter: null + name: all + module.billing-account.google_project_iam_member.bucket-sinks-binding["all"]: + condition: + - title: all bucket writer + role: roles/logging.bucketWriter + module.log-bucket-all.google_logging_project_bucket_config.bucket[0]: + bucket_id: billing-account-all + cmek_settings: [] + enable_analytics: false + location: global + locked: null + project: myprj + retention_days: 30 + +counts: + google_logging_billing_account_sink: 1 + google_logging_project_bucket_config: 1 + google_project_iam_member: 1 + modules: 2 + resources: 3 + +outputs: {} From a179508c355059a128e7921336b2ea79c0bf434b Mon Sep 17 00:00:00 2001 From: Ludo Date: Sat, 14 Oct 2023 14:28:28 +0200 Subject: [PATCH 3/6] folder module tfdoc --- modules/folder/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/folder/README.md b/modules/folder/README.md index 65661210dc..2ba7e9107a 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -272,7 +272,7 @@ module "folder" { | name | description | resources | |---|---|---| -| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding · google_folder_iam_member | +| [iam.tf](./iam.tf) | IAM bindings. | google_folder_iam_binding · google_folder_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_folder_iam_audit_config · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_compute_firewall_policy_association · google_essential_contacts_contact · google_folder | | [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy | @@ -295,7 +295,7 @@ module "folder" { | [id](variables.tf#L83) | Folder ID in case you use folder_create=false. | string | | null | | [logging_data_access](variables.tf#L89) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | | [logging_exclusions](variables.tf#L104) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L111) | Logging sinks to create for the organization. | map(object({…})) | | {} | +| [logging_sinks](variables.tf#L111) | Logging sinks to create for the folder. | map(object({…})) | | {} | | [name](variables.tf#L141) | Folder name. | string | | null | | [org_policies](variables.tf#L147) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | | [org_policies_data_path](variables.tf#L174) | Path containing org policies in YAML format. | string | | null | From e742935a243b7a062ae6805e5ff80cc07d6cbbb2 Mon Sep 17 00:00:00 2001 From: Ludo Date: Sat, 14 Oct 2023 14:38:16 +0200 Subject: [PATCH 4/6] remove redundant billing cost manager role in fast stage 0 --- fast/stages/0-bootstrap/billing.tf | 9 --------- fast/stages/0-bootstrap/organization-iam.tf | 15 +++++---------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/fast/stages/0-bootstrap/billing.tf b/fast/stages/0-bootstrap/billing.tf index 203ecbe865..8218546167 100644 --- a/fast/stages/0-bootstrap/billing.tf +++ b/fast/stages/0-bootstrap/billing.tf @@ -74,12 +74,3 @@ resource "google_billing_account_iam_member" "billing_ext_admin" { role = "roles/billing.admin" member = each.key } - -resource "google_billing_account_iam_member" "billing_ext_cost_manager" { - for_each = toset( - local.billing_mode == "resource" ? local.billing_ext_admins : [] - ) - billing_account_id = var.billing_account.id - role = "roles/billing.costsManager" - member = each.key -} diff --git a/fast/stages/0-bootstrap/organization-iam.tf b/fast/stages/0-bootstrap/organization-iam.tf index 9cc9966591..26285ac1e7 100644 --- a/fast/stages/0-bootstrap/organization-iam.tf +++ b/fast/stages/0-bootstrap/organization-iam.tf @@ -34,8 +34,7 @@ locals { authoritative = [] additive = ( local.billing_mode != "org" ? [] : [ - "roles/billing.admin", - "roles/billing.costsManager" + "roles/billing.admin" ] ) } @@ -66,8 +65,7 @@ locals { "roles/orgpolicy.policyAdmin" ], local.billing_mode != "org" ? [] : [ - "roles/billing.admin", - "roles/billing.costsManager" + "roles/billing.admin" ] ) } @@ -111,8 +109,7 @@ locals { "roles/orgpolicy.policyAdmin" ], local.billing_mode != "org" ? [] : [ - "roles/billing.admin", - "roles/billing.costsManager" + "roles/billing.admin" ] ) } @@ -129,8 +126,7 @@ locals { "roles/orgpolicy.policyAdmin" ], local.billing_mode != "org" ? [] : [ - "roles/billing.admin", - "roles/billing.costsManager" + "roles/billing.admin" ] ) } @@ -148,8 +144,7 @@ locals { # TODO: align additive roles with the README additive = ( local.billing_mode != "org" ? [] : [ - "roles/billing.admin", - "roles/billing.costsManager" + "roles/billing.admin" ] ) } From baed2933ed6940cf60d49e4390a659c40ef5d970 Mon Sep 17 00:00:00 2001 From: Ludo Date: Sat, 14 Oct 2023 15:05:26 +0200 Subject: [PATCH 5/6] fix FAST test --- tests/fast/stages/s0_bootstrap/simple.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fast/stages/s0_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml index b73cda2b0d..0afb9d02c6 100644 --- a/tests/fast/stages/s0_bootstrap/simple.yaml +++ b/tests/fast/stages/s0_bootstrap/simple.yaml @@ -18,7 +18,7 @@ counts: google_logging_organization_sink: 2 google_organization_iam_binding: 20 google_organization_iam_custom_role: 3 - google_organization_iam_member: 17 + google_organization_iam_member: 13 google_project: 3 google_project_iam_binding: 9 google_project_iam_member: 3 From a7ce02f805677a7e35adbd47b2f03f0412dac81b Mon Sep 17 00:00:00 2001 From: Ludo Date: Sun, 15 Oct 2023 16:44:01 +0200 Subject: [PATCH 6/6] address review comments --- modules/billing-account/budgets.tf | 30 ++++++---------------------- modules/billing-account/variables.tf | 2 +- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/modules/billing-account/budgets.tf b/modules/billing-account/budgets.tf index d3b695ed69..a1aed400fc 100644 --- a/modules/billing-account/budgets.tf +++ b/modules/billing-account/budgets.tf @@ -17,10 +17,8 @@ resource "google_monitoring_notification_channel" "default" { for_each = var.budget_notification_channels description = each.value.description - display_name = ( - each.value.display_name != null - ? each.value.display_name - : "Budget email notification ${each.key}." + display_name = coalesce( + each.value.display_name, "Budget email notification ${each.key}." ) project = each.value.project_id enabled = each.value.enabled @@ -72,26 +70,10 @@ resource "google_billing_budget" "default" { labels = each.value.filter.label == null ? null : { (each.value.filter.label.key) = each.value.filter.label.value } - projects = ( - each.value.filter.projects == null - ? null - : each.value.filter.projects - ) - resource_ancestors = ( - each.value.filter.resource_ancestors == null - ? null - : each.value.filter.resource_ancestors - ) - services = ( - each.value.filter.services == null - ? null - : each.value.filter.services - ) - subaccounts = ( - each.value.filter.subaccounts == null - ? null - : each.value.filter.subaccounts - ) + projects = each.value.filter.projects + resource_ancestors = each.value.filter.resource_ancestors + services = each.value.filter.services + subaccounts = each.value.filter.subaccounts dynamic "custom_period" { for_each = try(each.value.filter.period.custom, null) != null ? [""] : [] content { diff --git a/modules/billing-account/variables.tf b/modules/billing-account/variables.tf index 61f30005c3..6d949ea60d 100644 --- a/modules/billing-account/variables.tf +++ b/modules/billing-account/variables.tf @@ -102,7 +102,7 @@ variable "budgets" { try(v.amount.units, null) != null ) ]) - error_message = "Each budgets needs to have amount units specified, or use last period." + error_message = "Each budget needs to have amount units specified, or use last period." } validation { condition = alltrue(flatten([