Search code examples
azureterraformterraform-provider-azureazure-rm

How can I use azurerm_resource_group_template_deployment for azure budget resource but ignore changes in start and end date?


Maybe related: azurerm_resource_group_template_deployment ignoring parameter file

I would like to use the resource azurerm_resource_group_template_deployment from Terraform version 0.37. But there is the problem that Terraform wants to reapply the resource every month, so I thought I could tell to ignore changes to start date and end date, but this would (opposite to the deprecated resource azurerm_template_deployment) need a compute operation, namely jsondecode, which is not allowed. I.e. the following code would not work.

terraform {
  required_version = "~> 0.13.0"
  required_providers {
    azurerm = "~> 2.37.0"
  }
}

provider azurerm {
  features {}
}

locals {
  budget_start_date = formatdate("YYYY-MM-01", timestamp())
  budget_end_date = formatdate("YYYY-MM-01", timeadd(timestamp(), "17568h"))
  budget_params = jsonencode({
    "budgetName" = "budgettest",
    "amount" = "4000",
    "timeGrain" = "Annually",
    "startDate" = local.budget_start_date,
    "endDate" = local.budget_end_date,
    "firstThreshold" = "75",
    "secondThreshold" = "100",
    "thirdThreshold" = "50",
    "contactGroups" = ""
  }) 
  }

resource "azurerm_resource_group" "rg" {
  # A subscription cannot have more than 980 resource groups:
  # https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits
  name = "example-rg"
  location = "westeurope"
}

resource "azurerm_resource_group_template_deployment" "dsw_budget" {
  name = "test-budget-template"
  resource_group_name = azurerm_resource_group.rg[0].name
  deployment_mode = "Incremental"

  template_content = file("${path.module}/arm/budget_deploy.json")

  parameters_content = local.budget_params
  
  lifecycle {
    ignore_changes = [
      jsondecode(parameters_content)["startDate"],
      jsondecode(parameters_content)["endDate"]
    ]
  }

}

For the sake of completeness, content of budget_deploy.json:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "budgetName": {
      "type": "string",
      "defaultValue": "MyBudget"
    },
    "amount": {
      "type": "string",
      "defaultValue": "1000"
    },
    "timeGrain": {
      "type": "string",
      "defaultValue": "Monthly",
      "allowedValues": [
        "Monthly",
        "Quarterly",
        "Annually"
      ]
    },
    "startDate": {
      "type": "string"
    },
    "endDate": {
      "type": "string"
    },
    "firstThreshold": {
      "type": "string",
      "defaultValue": "90"
    },
    "secondThreshold": {
      "type": "string",
      "defaultValue": "110"
    },
    "thirdThreshold": {
      "type": "string",
      "defaultValue": "80"
    },
    "contactEmails": {
      "type": "string",
      "defaultValue": ""
    },
    "contactGroups": {
      "type": "string",
      "defaultValue": ""
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    }
  },
  "variables": {
    "groups": "[split(parameters('contactGroups'),',')]"
  },
  "resources": [
    {
      "name": "[parameters('budgetName')]",
      "type": "Microsoft.Consumption/budgets",
      "location": "[parameters('location')]",
      "apiVersion": "2019-10-01",
      "properties": {
        "timePeriod": {
          "startDate": "[parameters('startDate')]",
          "endDate": "[parameters('endDate')]"
        },
        "timeGrain": "[parameters('timeGrain')]",
        "amount": "[parameters('amount')]",
        "category": "Cost",
        "notifications": {
          "NotificationForExceededBudget1": {
            "enabled": true,
            "operator": "GreaterThan",
            "threshold": "[parameters('firstThreshold')]",
            "contactGroups": "[variables('groups')]"
          },
          "NotificationForExceededBudget2": {
            "enabled": true,
            "operator": "GreaterThan",
            "threshold": "[parameters('secondThreshold')]",
            "contactGroups": "[variables('groups')]"
          },
          "NotificationForExceededBudget3": {
            "enabled": true,
            "operator": "GreaterThan",
            "threshold": "[parameters('thirdThreshold')]",
            "contactGroups": "[variables('groups')]"
          }
        }
      }
    }
  ]
}

Is there any way that I can still achieve my goal? - thank you!


Solution

  • I resorted to use tags for the end and start date for the budget. The ignore_changes would work for the deprecated azurerm_template_deployment as parameters is of type map in that case and not of json type, like so:

    terraform {
      required_version = "~> 0.13.0"
      required_providers {
        azurerm = "~> 2.37.0"
      }
    }
    
    provider azurerm {
      features {}
    }
    
    locals {
      budget_start_date = formatdate("YYYY-MM-01", timestamp())
      budget_end_date = formatdate("YYYY-MM-01", timeadd(timestamp(), "17568h"))
      budget_params = {
        "budgetName" = "budgettest",
        "amount" = "4000",
        "timeGrain" = "Annually",
        "startDate" = local.budget_start_date,
        "endDate" = local.budget_end_date,
        "firstThreshold" = "75",
        "secondThreshold" = "100",
        "thirdThreshold" = "50",
        "contactGroups" = ""
      }
      }
    
    resource "azurerm_resource_group" "rg" {
      # A subscription cannot have more than 980 resource groups:
      # https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits
      name = "example-rg"
      location = "westeurope"
    }
    
    resource "azurerm_template_deployment" "dsw_budget" {
      name = "test-budget-template"
      resource_group_name = azurerm_resource_group.rg[0].name
      deployment_mode = "Incremental"
    
      template_content = file("${path.module}/arm/budget_deploy.json")
    
      parameters_content = local.budget_params
      
      lifecycle {
        ignore_changes = [
          parameters["startDate"],
          parameters["endDate"]
        ]
      }
    
    }
    

    Now this is not possible anymore with azurerm_resource_group_template_deployment, as json content is has to passed and therefore in ignore_changes a json-decoding which is a computation operation would have to be made, which is not allowed.

    Therefore to solve my problem of fixating start and end dates, I resorted to using tags for start and end date and a data source querying them:

    terraform {
      required_version = "~> 0.13.0"
      required_providers {
        azurerm = "~> 2.37.0"
      }
    }
    
    provider azurerm {
      features {
        template_deployment {
          delete_nested_items_during_deletion = false
        }
      }
    }
    
    data "azurerm_resources" "aml" {
      resource_group_name = "${var.tk_name_id}-${local.stage}-rg"
      type = "Microsoft.MachineLearningServices/workspaces"
    }
    
    
    locals {
      budget_start_date_tag = try(element(data.azurerm_resources.aml.resources[*].tags.budget_start_date, 0), "NA")
      budget_end_date_tag = try(element(data.azurerm_resources.aml.resources[*].tags.budget_end_date, 0), "NA")
      should_test_budget = local.is_test_stage_boolean && var.test_budget
      budget_start_date = local.budget_start_date_tag != "NA" ? local.budget_start_date_tag : (local.should_test_budget ? "START DATE FAIL!" : formatdate("YYYY-MM-01", timestamp()))
      budget_end_date = local.budget_end_date_tag != "NA" ? local.budget_end_date_tag : (local.should_test_budget ? "END DATE FAIL!" : formatdate("YYYY-MM-01", timeadd(timestamp(), "17568h")))
      budget_date_tags = {
         "budget_start_date" : local.budget_start_date,
         "budget_end_date" : local.budget_end_date
      }
    }
    
    #--------------------------------------------------------------------------------------------------------------------
    # DSW: Resource Group 
    # --------------------------------------------------------------------------------------------------------------------
    resource "azurerm_resource_group" "rg" {
      # A subscription cannot have more than 980 resource groups:
      # https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits
      count = local.no_addresses_available_boolean ? 0 : 1
      name = "test-rg"
      location = var.location
      tags = local.budget_date_tags
    }
    
    resource "azurerm_machine_learning_workspace" "aml_workspace" {
      name = local.aml_ws_name
      resource_group_name = azurerm_resource_group.rg[0].name
      location = azurerm_resource_group.rg[0].location
      application_insights_id = azurerm_application_insights.aml_insights.id
      key_vault_id = azurerm_key_vault.aml_kv.id
      storage_account_id = azurerm_storage_account.aml_st.id
      container_registry_id = azurerm_container_registry.aml_acr.id
      sku_name = "Basic"
      tags = merge(var.azure_tags, local.budget_date_tags)
      identity {
        type = "SystemAssigned"
      }
    }
    

    @Charles Xu I did not quite test it yet and I am also not sure if this is the best solution?

    EDIT: Now I actually run into cyclic dependency because the data source does obviously not exist before resource group is created: https://github.com/hashicorp/terraform/issues/16380.