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.
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.
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.
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.
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.
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.
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"
}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 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.
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.
| 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 |
| Name | Version |
|---|---|
| azurerm | >= 4.0.0, < 5.0.0 |
| random | >= 3.8.1, < 4.0.0 |
| time | >= 0.10.0, < 1.0.0 |
No modules.
| 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 |
| 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 |
| Name | Description |
|---|---|
| output | n/a |