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
- User Managed Identity for communication between Storage Account and Key Vault
- Hardened Storage Account with local Data Protection settings and Customer Managed Key encryption
- 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
|
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.15.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.0"
}
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}-${formatdate("YYYYMMDD-hhmm", time_static.current.rfc3339)}"
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
}
resource "azurerm_key_vault_key" "this" {
key_opts = ["wrapKey", "unwrapKey"]
key_type = "RSA"
key_size = 2048
key_vault_id = azurerm_key_vault.this.id
name = local.key_name
lifecycle {
create_before_destroy = true
}
}
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"
account_replication_type = "ZRS"
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
shared_access_key_enabled = false
allow_nested_items_to_be_public = false
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 {
ip_rules = ["${chomp(data.http.icanhazip.response_body)}"]
bypass = ["Logging", "Metrics", "AzureServices"]
default_action = "Deny"
}
blob_properties {
change_feed_enabled = true
change_feed_retention_in_days = 15
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_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
|
variable "appname" {
description = "Unique identification"
type = string
}
variable "subscription_id" {
description = "Subscription ID for all resources"
type = string
}
variable "location" {
description = "Location for all resources"
type = string
}
|
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"
}
|