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:
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.

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"
}
|