So you want to deploy a PostgreSQL database in a private vnet and link it to your custom application in Azure. You have developed this awesome app thrown it into a container image and now you need to build the infrastructure to host your beautiful code. You could click around the disgusting Azure GUI like a pleb or you can be cool and deploy your infrastructure using code. With Terraform being your poison of choice to achieve this don’t worry, you have come to the right place.

To kick ourselves off let’s set up our Terraform backend. Sadly for this step, we do need to set it up manually. Go to Azure and create a resource group, storage account, and storage container in that storage account. Then put those details into our Backup.tf file in your project.

# Backend.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "tf-state-rg"
    storage_account_name = "tfstate"
    container_name       = "tfstate"
    key                  = "coolapp.terraform.tfstate"
  }
}

Now everything we do to our environment will be saved to a remote state file. Now let’s start building that infrastructure! Starting with that Resource Group and Virtual Network that we will be deploying all our infrastructure into. We also add our subscription as data elements. I would also like to add that I’m assuming you will be adding the var elements to your variables.tf file.

# Main.tf
data "azurerm_client_config" "current" {}

data "azurerm_subscription" "current" {}

# Create a resource group
resource "azurerm_resource_group" "rg" {
  name     = lower("${var.rg_name}-${var.environment}")
  location = var.rg_location

  tags = {
    environment = var.environment
  }
}

# Create Virtual Network
resource "azurerm_virtual_network" "vnet" {
  name                = lower("${var.vnet_name}-${var.environment}")
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  address_space       = var.vnet_address_space

  tags = {
    environment = var.environment
  }

  depends_on = [
    azurerm_resource_group.rg
  ]
}

To prepare for the database I want to deploy a key vault to keep my secrets safe and secure. This step is optional as you can always deploy the database without storing the admin credentials in a vault but I want to do it this way. To achieve this we must deploy a dedicated subnet and vault. Let’s start with the subnet.

# Vault.tf
# Vault Subnet
resource "azurerm_subnet" "vault_subnet" {
  name                                          = var.vault_subnet_name
  resource_group_name                           = azurerm_virtual_network.vnet.resource_group_name
  virtual_network_name                          = azurerm_virtual_network.vnet.name
  address_prefixes                              = var.vault_subnet_address_prefixes
  private_endpoint_network_policies_enabled     = false
  private_link_service_network_policies_enabled = false
  service_endpoints                             = ["Microsoft.KeyVault"]
}

# Vault DNS zone
resource "azurerm_private_dns_zone" "vault_dns_zone" {
  name                = "cool-app-privatelink.vaultcore.azure.net"
  resource_group_name = azurerm_virtual_network.vnet.resource_group_name

  depends_on = [
    azurerm_virtual_network.vnet
  ]
}

# Private virtual network link to vnet
resource "azurerm_private_dns_zone_virtual_network_link" "vault_dns_zone_vnet_link" {
  name                  = "privatelink_to_${azurerm_virtual_network.vnet.name}"
  resource_group_name   = azurerm_virtual_network.vnet.resource_group_name
  virtual_network_id    = azurerm_virtual_network.vnet.id
  private_dns_zone_name = azurerm_private_dns_zone.vault_dns_zone.name

  lifecycle {
    ignore_changes = [
      tags
    ]
  }
  depends_on = [
    azurerm_resource_group.rg,
    azurerm_virtual_network.vnet,
    azurerm_private_dns_zone.vault_dns_zone
  ]
}

Now that we have our subnet let’s deploy our vault into said subnet.

# Vault.tf
# Randomize part of name
resource "random_string" "prefix" {
  length           = 4
  special          = true
  override_special = "abcdefghijklmnopqrstuvwxyz"
}

# Key Vault
resource "azurerm_key_vault" "key_vault" {
  name                = "${var.key_vault_name}-${var.environment}-${random_string.prefix.result}"
  resource_group_name = azurerm_virtual_network.vnet.resource_group_name
  location            = azurerm_virtual_network.vnet.location
  tenant_id           = data.azurerm_subscription.current.tenant_id
  sku_name            = var.key_vault_sku

  soft_delete_retention_days    = 7
  purge_protection_enabled      = false
  enabled_for_disk_encryption   = true
  public_network_access_enabled = true


  network_acls {
    default_action = "Allow"
    bypass = "AzureServices"
  }

  access_policy {
    tenant_id = data.azurerm_subscription.current.tenant_id
    object_id = data.azurerm_client_config.current.object_id

    key_permissions         = var.kv_key_permissions
    secret_permissions      = var.kv_secret_permissions
    certificate_permissions = var.kv_certificate_permissions
    storage_permissions     = var.kv_storage_permissions
  }
  depends_on = [
    random_string.prefix,
    azurerm_subnet.vault_subnet,
  ]
}

# Create private endpoint for key vault
resource "azurerm_private_endpoint" "vault_private_endpoint" {
  name                = lower("PrivateEndpoint-${azurerm_key_vault.key_vault.name}")
  location            = azurerm_virtual_network.vnet.location
  resource_group_name = azurerm_virtual_network.vnet.resource_group_name
  subnet_id           = azurerm_subnet.psql_config_subnet.id

  private_service_connection {
    name                           = "pe-${azurerm_key_vault.key_vault.name}"
    private_connection_resource_id = azurerm_key_vault.key_vault.id
    is_manual_connection           = false
    subresource_names              = ["vault"]
  }

  private_dns_zone_group {
    name                 = "default" 
    private_dns_zone_ids = [azurerm_private_dns_zone.vault_dns_zone.id]
  }

  lifecycle {
    ignore_changes = [
      tags
    ]
  }
  depends_on = [
    azurerm_key_vault.key_vault,
    azurerm_private_dns_zone.vault_dns_zone
  ]
}

You will notice a couple of issues with the above configuration.
1) We are using access policies instead of RBAC. You can set up RBAC if you want with the vault, which is in line with best practice, but to keep this post relatively simple I’m going to use access policies and link my Azure user account to be the only user that can access this vault. A quick Google search will get you where you need to go.
2) We have network_acls set to default allow. This isn’t ideal either as anything can connect to this in theory. This is however locked down by our access policy only allowing me to access this. The reason I did this I wanted to stick to the restriction of not going into the GUI to add secrets or I would have to build in a “jump box” with access to the private subnet to add these secrets… I could do this buuut I don’t want this post to go on forever!

Anyhow fantastic we have set up our Key Vault in a Subnet with an endpoint that we can hit.

Now let’s do what we came here to do, deployment of the Postgres server! Firstly Azure requires that the PostgreSQL server is deployed in a dedicated subnet with a private endpoint. So let us do that!

# PostgreSQL.tf
# PostgreSQL Subnet
resource "azurerm_subnet" "psql_subnet" {
  name                                          = var.psql_subnet_name
  resource_group_name                           = azurerm_virtual_network.vnet.resource_group_name
  virtual_network_name                          = azurerm_virtual_network.vnet.name
  address_prefixes                              = var.psql_subnet_address_prefixes
  private_endpoint_network_policies_enabled     = false
  private_link_service_network_policies_enabled = false
  service_endpoints                             = ["Microsoft.Storage", "Microsoft.KeyVault"]

  delegation {
    name = "postgreSQLdelegation"
    service_delegation {
      name = "Microsoft.DBforPostgreSQL/flexibleServers"
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action",
      ]
    }
  }

  depends_on = [
    azurerm_virtual_network.vnet
  ]

}
# PostgreSQL DNS Zone
resource "azurerm_private_dns_zone" "psql_dns_zone" {
  name                = "${var.psql_server_name}-${var.environment}.private.postgres.database.azure.com"
  resource_group_name = azurerm_virtual_network.vnet.resource_group_name
  # Tags

  depends_on = [
    azurerm_virtual_network.vnet
  ]
}
# Link that up
resource "azurerm_private_dns_zone_virtual_network_link" "psql_dns_zone_link" {
  name                  = "link_to_${azurerm_virtual_network.vnet.name}"
  resource_group_name   = azurerm_virtual_network.vnet.resource_group_name
  private_dns_zone_name = azurerm_private_dns_zone.psql_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.vnet.id

  depends_on = [
    azurerm_private_dns_zone.psql_dns_zone,
    azurerm_virtual_network.vnet
  ]
}

We will also need to create the credentials required for the PostgreSQL server.

# PostgreSQL.tf
# Create admin user password
resource "random_password" "psql_admin_password" {
  length           = 20
  special          = true
  lower            = true
  upper            = true
  override_special = "!#"
}

# Create admin user & password in vault
resource "azurerm_key_vault_secret" "psql_admin_username" {
  name         = "postgres-db-username"
  value        = var.psql_admin_login
  key_vault_id = azurerm_key_vault.key_vault.id
  tags         = {}
  depends_on = [
    azurerm_key_vault.key_vault,
  ]
}

resource "azurerm_key_vault_secret" "psql_admin_password" {
  name         = "postgres-db-password"
  value        = random_password.psql_admin_password.result
  key_vault_id = azurerm_key_vault.key_vault.id
  tags         = {}
  depends_on = [
    azurerm_key_vault.key_vault,
    random_password.psql_admin_password,
  ]
}

With our dedicated subnet deployed, key vault deployed, and credentials for the PostgreSQL server-generated and injected into the vault… IT IS TIME TO DEPLOY THE SERVER!!!!!!

# PostgreSQL.tf
# PostgreSQL Server
resource "azurerm_postgresql_flexible_server" "psql_server" {
  name                = lower("${var.psql_server_name}-${var.environment}")
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  delegated_subnet_id = azurerm_subnet.psql_subnet.id
  private_dns_zone_id = azurerm_private_dns_zone.psql_dns_zone.id
  zone                = 1

  administrator_login    = azurerm_key_vault_secret.psql_admin_username.value
  administrator_password = azurerm_key_vault_secret.psql_admin_password.value

  version    = var.psql_server_version
  sku_name   = var.psql_server_sku_name
  storage_mb = var.psql_server_storage_mb

  backup_retention_days = 7

  depends_on = [
    azurerm_resource_group.rg,
    azurerm_subnet.psql_subnet,
    azurerm_private_dns_zone.psql_dns_zone,
    azurerm_key_vault_secret.psql_admin_username,
    azurerm_key_vault_secret.psql_admin_password
  ]
}

# PostgreSQL Database
resource "azurerm_postgresql_flexible_server_database" "psql_db" {
  name      = var.psql_database_name
  server_id = azurerm_postgresql_flexible_server.psql_server.id
  charset   = "utf8"
  collation = "en_US.utf8"

lifecycle {
   prevent_destroy = false
}

  depends_on = [
    azurerm_postgresql_flexible_server.psql_server
  ]
}

CONGRATULATIONS! You have deployed a PostgreSQL DB with a key vault ready to be used by the awesomely cool app you want to share with the world. We can mount the necessary credentials into the environment variables of the container for our script to use.

There are several other things I could build on. Whether that be the configuration of the database on the private network or the scheduled execution of your apps’ containers. I’ll see if I write up a dedicated blog post for it.

Now if you have read this far I truly thank you from the bottom of my heart…
NOW GO TOUCH SOME GRASS 😀

EpicBemo