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

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

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
Built with Hugo
Theme Stack designed by Jimmy