Skip to content

About

Terraform module for setting up Azure cost exports for Stacklet

Topics

Resources

Stars

0 stars

Watchers

3 watching

Forks

Repository files navigation

terraform-azure-stacklet-cost-setup

Azure Cost Management export setup for Stacklet customers.

This module configures one Azure subscription to export its cost data to a Storage Account, in the format that the Stacklet platform reads. The platform runs outside the subscription, so the module also grants read access on that Storage Account to a service principal that you create and that Stacklet authenticates as.

For background, and for the steps to do the same thing through the Azure portal, see the Stacklet documentation.

Overview

Apply this module once per subscription. The user or service principal that applies it needs permission to create Cost Management exports, Storage Accounts, and Resource Groups, and to assign roles on the Storage Account it creates. Owner covers all of it. Contributor does not: it cannot assign roles, and the apply fails at that step. Pair Contributor with Role Based Access Control Administrator or User Access Administrator to keep the narrower grant.

The module creates:

  • a Resource Group that holds the resources below
  • a Storage Account for the cost management exports
  • a daily Cost Management export job that writes to the Storage Account
  • a Storage Blob Data Reader assignment on the Storage Account for Stacklet

The job exports in the format that Stacklet needs.

Before you apply

Create an Entra ID service principal for Stacklet in your tenant. The Stacklet documentation has the steps. Keep its Tenant ID, Client ID, and Client Secret, and send those to Stacklet separately. This module never sees them.

The module needs the service principal's object ID:

az ad sp show --id <client-id> --query id -o tsv

Pass that as stacklet_principal_id. The object ID and the Client ID are both GUIDs, and people swap them by mistake, so the module keeps the directory check on. Azure rejects an ID that names no principal, and the apply fails rather than record a grant that gives Stacklet nothing. A successful apply means the grant landed on a real principal.

A wrong ID shows up as PrincipalNotFound. The provider cannot tell that apart from replication lag on a new service principal, so it retries for five minutes before it gives up. An apply that sits on the role assignment for minutes usually means the ID is wrong, not that Azure is slow.

What to send Stacklet

After the apply, terraform output output reports the storage account name, the container URL, and the subscription ID. Those are what the documentation asks you to hand over. You do not need to collect anything from the portal.

Versioning

This module is published to the Terraform Registry. Set source to the registry address and pin version to a release.

version takes the usual constraint syntax, so ~> 1.0 accepts patch and minor releases and holds at the next major. A major bump means the root module that calls this one needs work before it moves. Read the release notes first.

Provider Configuration

This module does not configure providers. The calling module must configure the azurerm provider with the target subscription. azurerm 4.0 and later require subscription_id on the provider block. The provider does not read the subscription that az login selected.

Credentials come from the standard Azure authentication chain. For local use, authenticate with az login. For automated deployments, supply credentials through environment variables (ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID) or set client_id, client_secret, and tenant_id directly on the provider block.

Standalone deployment

Applying the cost export to a single subscription:

provider "azurerm" {
  features {}
  subscription_id = "your-subscription-id"
}

module "azure_cost_setup" {
  source  = "stacklet/stacklet-cost-setup/azure"
  version = "1.0.0"

  customer_prefix         = "your-prefix"
  resource_group_location = "eastus"
  stacklet_principal_id   = "00000000-0000-0000-0000-000000000000"
}

Several subscriptions in one root module

Apply the module once per subscription. Give each subscription an aliased provider and pass that provider to the module:

provider "azurerm" {
  features {}
  subscription_id = "management-subscription-id"
}

provider "azurerm" {
  alias           = "workload"
  subscription_id = "workload-subscription-id"
  features {}
}

module "azure_cost_setup_workload" {
  source  = "stacklet/stacklet-cost-setup/azure"
  version = "1.0.0"

  providers = {
    azurerm = azurerm.workload
  }

  customer_prefix         = "your-prefix"
  resource_group_location = "eastus"
  stacklet_principal_id   = "00000000-0000-0000-0000-000000000000"
}

One service principal can serve every subscription. Pass its object ID in each module block. The module grants it read access on each Storage Account it creates.

The cost export window

The azurerm provider requires an end date on a cost export, so the module writes a window rather than an open-ended schedule. The window is ten years long by default and the module renews it after five. Both come from export_window_years. Renewal happens on the next plan and apply after the half-way point, and it rewrites the dates on the export in place. Renewal does not touch cost data already in the Storage Account.

If an apply does not happen between the half-way point and the expiration, the export stops. Applying again puts it back on schedule. Changing export_window_years also forces a fresh window from the time of the apply, so a shorter window never lands in the past.

Deleting the export by hand, or recreating it outside a normal apply, leaves the module holding a start date Azure no longer accepts, and the next apply fails with 'from' value cannot be in the past. That does not self-correct, because the resource to replace is the start date rather than the export:

terraform apply -replace=module.azure_cost_setup.time_rotating.export_window_start

Use your own module block name. Terraform reports no changes rather than an error when a -replace address matches nothing.

Migrating to 1.0.0

1.0.0 is the first release, so whatever you have now is pinned to a commit rather than to a version. Move source to the registry address and set version, as shown above.

Read your current copy rather than a version number. If it declares its own azurerm provider block, that is the block 1.0.0 removes. A root module that supplied no provider relied on it. Add an azurerm provider block to your root module and set subscription_id on it.

If you apply this repository directly from a checkout, azurerm now stops at plan and asks for explicit configuration, because its features block has no default. Consume the module from your own root module instead, as shown above. That is the pattern this module is built for, and it keeps the provider configuration with the caller who owns the subscription.

stacklet_principal_id is new and required, so an existing root module stops at plan until you supply it. If you already granted Stacklet Storage Blob Data Reader by hand, pass the same principal's object ID and import the assignment you made. Azure refuses a second assignment for the same principal, role, and scope, so an apply without the import fails with RoleAssignmentExists:

terraform import module.azure_cost_setup.azurerm_role_assignment.stacklet_cost_reader \
  "<storage-account-resource-id>/providers/Microsoft.Authorization/roleAssignments/<assignment-guid>"

The output map also drops its storage_container key for container_url. Despite its name, that key already held the container URL, so the value does not change. Anything reading it needs a rename and nothing more.

Requirements

Name Version
terraform >= 1.14.0, < 2.0.0
azurerm >= 4.0.0, < 5.0.0
random >= 3.8.1, < 4.0.0
time >= 0.10.0, < 1.0.0

Providers

Name Version
azurerm >= 4.0.0, < 5.0.0
random >= 3.8.1, < 4.0.0
time >= 0.10.0, < 1.0.0

Modules

No modules.

Resources

Name Type
azurerm_resource_group.current resource
azurerm_role_assignment.stacklet_cost_reader resource
azurerm_storage_account.cost resource
azurerm_storage_container.cost resource
azurerm_subscription_cost_management_export.cost resource
random_string.storage_account_suffix resource
time_rotating.export_window_start resource
azurerm_subscription.current data source

Inputs

Name Description Type Default Required
customer_prefix Stacklet provided customer prefix string n/a yes
export_window_years Length in years of the cost export schedule. The module renews the window once half of it passes, and changing this forces a fresh window from the time of the apply. Renewal needs an apply to happen. See the cost export window section of the README. number 10 no
resource_group_location Resource group deployment location string n/a yes
stacklet_principal_id Object ID of the Entra ID service principal that Stacklet reads the cost export with. This is the service principal's object ID, not the application (client) ID of the app registration behind it. string n/a yes

Outputs

Name Description
output n/a

About

Terraform module for setting up Azure cost exports for Stacklet

Topics

Resources

Stars

0 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages