Featured image of post Terraform AzureRM Backend Automation

Terraform AzureRM Backend Automation

In this article I will show a Terraform project that covers the key aspects of my previous article.

When I wrote the previous article, most of the configuration was a very unreliable powershell script. In this article I will show a Terraform project that covers the key aspects of my previous article.

Covered:

  • Azure Key Vault ready for Customer Managed Key Handling
  • Azure Key Vault Key with Rotation Policy
  • User Managed Identity for communication between Azure Storage Account and Azure Key Vault
  • Hardened Azure Storage Account with local Data Protection settings and Customer Managed Key encryption
  • Basic Azure Storage Account Management Policy to cleanup old Snapshots and Versions
  • Storage Container

NOT covered:

  • Backup
  • Logging

Creating something with Terrafrom, which in turn is a prerequisite for a Terraform configuration, seems a bit strange. But from my point of view it makes perfect sense. Even if you use the configuration as a ‘fire and forget’ script, it is still very elegant and robust, but it is also possible to maintain all your backends in a central configuration. Smaller states/projects are often a way to reduce the blastradius or to draw boundaries of responsibility.

The following code can also be found in this GitHub repository: https://github.com/vMarkusK/terraform-azurerm-backend

Main

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 4.25.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.6.3"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = ">= 3.0.2"
    }
    http = {
      source  = "hashicorp/http"
      version = ">= 3.4.5"
    }
  }
  required_version = ">= 1.10.5"
}

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id

  storage_use_azuread = true
}

data "azuread_client_config" "this" {}

locals {
  rg_name  = "rg-${var.appname}-${random_string.suffix.result}"
  uai_name = "uai-${var.appname}-${random_string.suffix.result}"
  kv_name  = "kv-${var.appname}-${random_string.suffix.result}"
  key_name = "cmk-${var.appname}-${random_string.suffix.result}"
  st_name  = "st${var.appname}${random_string.suffix.result}"
}

resource "time_static" "current" {}

data "http" "icanhazip" {
  url = "http://ipv4.icanhazip.com"
}

resource "random_string" "suffix" {
  length  = 6
  special = false
  upper   = false
  numeric = true
  lower   = true
}

resource "azurerm_resource_group" "this" {
  name     = local.rg_name
  location = var.location
}

resource "azurerm_user_assigned_identity" "this" {
  name                = local.uai_name
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location
}

resource "azurerm_key_vault" "this" {
  name                = local.kv_name
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location

  sku_name                   = "standard"
  tenant_id                  = data.azuread_client_config.this.tenant_id
  enable_rbac_authorization  = true
  purge_protection_enabled   = true
  soft_delete_retention_days = 7

  network_acls {
    default_action = "Deny"
    bypass         = "AzureServices"
    ip_rules       = ["${chomp(data.http.icanhazip.response_body)}/32"]
  }

}

resource "azurerm_role_assignment" "uai" {
  scope                = azurerm_key_vault.this.id
  role_definition_name = "Key Vault Crypto User"
  principal_id         = azurerm_user_assigned_identity.this.principal_id
  principal_type       = "ServicePrincipal"
}

resource "azurerm_key_vault_key" "this" {
  name         = local.key_name
  key_vault_id = azurerm_key_vault.this.id


  key_type = "RSA"
  key_size = 4096
  key_opts = ["wrapKey", "unwrapKey"]

  expiration_date = timeadd(formatdate("YYYY-MM-DD'T'HH:mm:ss'Z'", time_static.current.rfc3339), "8760h")

  rotation_policy {
    automatic {
      time_before_expiry = "P60D"
    }

    expire_after         = "P365D"
    notify_before_expiry = "P30D"
  }

  lifecycle {
    ignore_changes = [expiration_date]
  }
}

resource "azurerm_storage_account" "this" {
  name                = local.st_name
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location

  account_kind             = "StorageV2"
  account_tier             = "Standard"
  access_tier              = "Hot"
  account_replication_type = "ZRS"

  public_network_access_enabled    = true
  https_traffic_only_enabled       = true
  min_tls_version                  = "TLS1_2"
  shared_access_key_enabled        = false
  allow_nested_items_to_be_public  = false
  cross_tenant_replication_enabled = false
  sftp_enabled                     = false
  local_user_enabled               = false
  queue_encryption_key_type        = "Account"
  table_encryption_key_type        = "Account"


  identity {
    type = "UserAssigned"
    identity_ids = [
      azurerm_user_assigned_identity.this.id
    ]
  }

  customer_managed_key {
    key_vault_key_id          = azurerm_key_vault_key.this.id
    user_assigned_identity_id = azurerm_user_assigned_identity.this.id
  }

  network_rules {
    default_action = "Deny"
    bypass         = ["Logging", "Metrics", "AzureServices"]
    ip_rules       = ["${chomp(data.http.icanhazip.response_body)}"]
  }

  share_properties {
    retention_policy {
      days = 14
    }
  }

  blob_properties {
    change_feed_enabled           = true
    change_feed_retention_in_days = 60
    versioning_enabled            = true

    restore_policy {
      days = 7
    }

    container_delete_retention_policy {
      days = 14
    }

    delete_retention_policy {
      days = 14
    }

  }

  depends_on = [azurerm_key_vault.this, azurerm_role_assignment.uai]
}

resource "azurerm_storage_management_policy" "this" {
  storage_account_id = azurerm_storage_account.this.id

  rule {
    name    = "cleanup"
    enabled = true
    filters {
      blob_types = ["blockBlob"]
    }
    actions {
      snapshot {
        delete_after_days_since_creation_greater_than = 30
      }
      version {
        delete_after_days_since_creation = 30
      }
    }
  }
}

resource "azurerm_storage_container" "this" {
  name                  = "tfstate"
  storage_account_id    = azurerm_storage_account.this.id
  container_access_type = "private"
}

Provider configuration

The optional parameter storage_use_azuread = true is required to properly handle the storage container configuration when key access is disabled.

Dependencies of the Storage Account

The configuration includes two indirect dependencies that must be configured on the storage account resource.

  • User Managed Identity Role Assignment
  • Key Vault

Variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
variable "appname" {
  description = "Unique identification"
  type        = string
  default     = "tfstate"

}
variable "subscription_id" {
  description = "Subscription ID for all resources"
  type        = string
}

variable "location" {
  description = "Location for all resources"
  type        = string
  default     = "germywestcentral"
}

Outputs

The outputs are designed to match the required Terraform backend configuration.

Terraform AzureRM Backend Automation - Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
output "resource_group_name" {
  value = azurerm_resource_group.this.name
}

output "storage_account_name" {
  value = azurerm_storage_account.this.name
}

output "container_name" {
  value = "tfstate"
}

output "key" {
  value = "${var.appname}.terraform.tfstate"
}

output "use_azuread_auth" {
  value = "true"
}
Licensed under CC BY-NC-SA 4.0
Last updated on May 06, 2025 00:00 UTC
Built with Hugo
Theme Stack designed by Jimmy