Billing budget module

This commit is contained in:
Julio Castillo 2021-08-12 19:43:09 +02:00
parent cb7c65135e
commit 3a8a040ff3
9 changed files with 472 additions and 0 deletions

View File

@ -0,0 +1,95 @@
# 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
resource "google_monitoring_notification_channel" "channel" {
display_name = "$100 spend channel"
type = "email"
project = var.project_id
labels = {
email_address = "user@example.com"
}
}
module "budget" {
source = "./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"
]
notification_channels = [
google_monitoring_notification_channel.channel.id
]
}
# tftest:modules=1:resources=1
```
### 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 = "./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 = "./modules/pubsub"
project_id = var.project_id
name = "budget-topic"
}
# tftest:modules=2:resources=2
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
| billing_account | Billing account id. | <code title="">string</code> | ✓ | |
| name | Budget name. | <code title="">string</code> | ✓ | |
| thresholds | None | <code title="object&#40;&#123;&#10;current &#61; list&#40;number&#41;&#10;forecasted &#61; list&#40;number&#41;&#10;&#125;&#41;&#10;validation &#123;&#10;condition &#61; length&#40;var.thresholds.current&#41; &#62; 0 &#124;&#124; length&#40;var.thresholds.forecasted&#41; &#62; 0&#10;error_message &#61; &#34;Must specify at least one budget threshold.&#34;&#10;&#125;">object({...})</code> | ✓ | |
| *amount* | Amount in the billing account's currency for the budget. Use 0 to set budget to 100% of last period's spend. | <code title="">number</code> | | <code title="">0</code> |
| *credit_treatment* | How credits should be treated when determining spend for threshold calculations. Only INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS are supported | <code title="">string</code> | | <code title="INCLUDE_ALL_CREDITS&#10;validation &#123;&#10;condition &#61; &#40;&#10;var.credit_treatment &#61;&#61; &#34;INCLUDE_ALL_CREDITS&#34; &#124;&#124;&#10;var.credit_treatment &#61;&#61; &#34;EXCLUDE_ALL_CREDITS&#34;&#10;&#41;&#10;error_message &#61; &#34;Argument credit_treatment must be INCLUDE_ALL_CREDITS or EXCLUDE_ALL_CREDITS.&#34;&#10;&#125;">...</code> |
| *notification_channels* | Monitoring notification channels (up to 5) where to send updates. | <code title="list&#40;string&#41;">list(string)</code> | | <code title="">null</code> |
| *notify_default_recipients* | Notify Billing Account Administrators and Billing Account Users IAM roles for the target account. | <code title="">bool</code> | | <code title="">false</code> |
| *projects* | 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. | <code title="list&#40;string&#41;">list(string)</code> | | <code title="">null</code> |
| *pubsub_topic* | The ID of the Cloud Pub/Sub topic where budget related messages will be published. | <code title="">string</code> | | <code title="">null</code> |
| *services* | 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. | <code title="list&#40;string&#41;">list(string)</code> | | <code title="">null</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| budget | Budget resource. | |
| id | Budget ID. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,78 @@
/**
* Copyright 2021 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
}
]
])
}
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 = var.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"
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright 2021 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 = "Budget ID."
value = google_billing_budget.budget.id
}

View File

@ -0,0 +1,85 @@
/**
* Copyright 2021 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 "name" {
description = "Budget name."
type = string
}
variable "notification_channels" {
description = "Monitoring notification channels (up to 5) 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" {
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."
}
}

View File

@ -0,0 +1,23 @@
/**
* Copyright 2021 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.
*/
terraform {
required_version = ">= 0.13.0"
required_providers {
google = ">= 3.79.0"
google-beta = ">= 3.79.0"
}
}

View File

@ -0,0 +1,13 @@
# Copyright 2021 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.

View File

@ -0,0 +1,29 @@
/**
* Copyright 2021 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.
*/
module "budget" {
source = "../../../../modules/billing-budget"
billing_account = "123456-123456-123456"
name = "my budget"
projects = var.projects
services = var.services
notify_default_recipients = var.notify_default_recipients
amount = var.amount
credit_treatment = var.credit_treatment
pubsub_topic = var.pubsub_topic
notification_channels = var.notification_channels
thresholds = var.thresholds
}

View File

@ -0,0 +1,61 @@
/**
* Copyright 2021 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" {
type = number
default = 0
}
variable "credit_treatment" {
type = string
default = "INCLUDE_ALL_CREDITS"
}
variable "notification_channels" {
type = list(string)
default = null
}
variable "notify_default_recipients" {
type = bool
default = false
}
variable "projects" {
type = list(string)
default = null
}
variable "pubsub_topic" {
type = string
default = null
}
variable "services" {
type = list(string)
default = null
}
variable "thresholds" {
type = object({
current = list(number)
forecasted = list(number)
})
default = {
current = [0.5, 1.0]
forecasted = [1.0]
}
}

View File

@ -0,0 +1,63 @@
# Copyright 2021 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.
import os
import pytest
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture')
def test_resource_count(plan_runner):
"Test number of resources created."
_, resources = plan_runner(FIXTURES_DIR, pubsub_topic='topic')
assert len(resources) == 1
resource = resources[0]
assert resource['values']['all_updates_rule'] == [
{'disable_default_iam_recipients': False,
'monitoring_notification_channels': None,
'pubsub_topic': 'topic',
'schema_version': '1.0'}
]
_, resources = plan_runner(FIXTURES_DIR, notification_channels='["channel"]')
assert len(resources) == 1
resource = resources[0]
assert resource['values']['all_updates_rule'] == [
{'disable_default_iam_recipients': True,
'monitoring_notification_channels': ['channel'],
'pubsub_topic': None,
'schema_version': '1.0'}
]
def test_absolute_amount(plan_runner):
"Test absolute amount budget."
_, resources = plan_runner(FIXTURES_DIR, pubsub_topic='topic', amount="100")
assert len(resources) == 1
resource = resources[0]
amount = resource['values']['amount'][0]
assert amount['last_period_amount'] is None
assert amount['specified_amount'] == [{'nanos': None, 'units': '100'}]
assert resource['values']['threshold_rules'] == [
{'spend_basis': 'CURRENT_SPEND',
'threshold_percent': 0.5},
{'spend_basis': 'CURRENT_SPEND',
'threshold_percent': 1},
{'spend_basis': 'FORECASTED_SPEND',
'threshold_percent': 1}
]