Skip to content

Instantly share code, notes, and snippets.

@ChrisRomp
Last active September 3, 2021 22:41
Show Gist options
  • Save ChrisRomp/c5b97465c915359829ac1b35b6b403ca to your computer and use it in GitHub Desktop.
Save ChrisRomp/c5b97465c915359829ac1b35b6b403ca to your computer and use it in GitHub Desktop.
Terraform Function App with Encrypted Storage and VNet Isolation
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>2.74.0"
}
}
}
# data "azurerm_client_config" "current" {}
# Configure the Microsoft Azure Provider
provider "azurerm" {
features {}
tenant_id = var.tenant_id
subscription_id = var.subscription_id
}
# Create a resource group
resource "azurerm_resource_group" "func" {
name = var.resource_group_name
location = var.resource_group_location
}
# VNet
resource "azurerm_virtual_network" "func" {
name = format("%s%s%s", azurerm_resource_group.func.name, var.function_app_name, var.vnet_name)
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
address_space = ["10.1.0.0/16"]
}
# Default subnet
resource "azurerm_subnet" "func-default" {
name = "default"
resource_group_name = azurerm_resource_group.func.name
virtual_network_name = azurerm_virtual_network.func.name
address_prefixes = ["10.1.0.0/24"]
enforce_private_link_endpoint_network_policies = true
service_endpoints = [
"Microsoft.Storage",
"Microsoft.KeyVault",
]
}
# Apps subnet
resource "azurerm_subnet" "func-apps" {
name = "apps"
resource_group_name = azurerm_resource_group.func.name
virtual_network_name = azurerm_virtual_network.func.name
address_prefixes = ["10.1.1.0/24"]
delegation {
name = "delegation"
service_delegation {
name = "Microsoft.Web/serverFarms"
actions = [ "Microsoft.Network/virtualNetworks/subnets/action" ]
}
}
service_endpoints = ["Microsoft.Storage"]
}
# Random string for KV to avoid vault purge/recreate issue
resource "random_string" "vault-id" {
length = 5
special = false
upper = false
number = false
}
# Key Vault
resource "azurerm_key_vault" "func" {
name = format("%s%s", azurerm_resource_group.func.name, "${var.vault_name}-${random_string.vault-id.result}")
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
tenant_id = var.tenant_id
sku_name = "standard"
purge_protection_enabled = true # required for customer-managed key setup?
soft_delete_retention_days = 7 # minimum value
network_acls {
bypass = "AzureServices"
default_action = "Deny"
virtual_network_subnet_ids = [azurerm_subnet.func-default.id]
ip_rules = [ "${var.my_ip}/32" ]
}
# access_policy = []
}
# Deployment account key access
resource "azurerm_key_vault_access_policy" "deployment-account" {
key_vault_id = azurerm_key_vault.func.id
tenant_id = var.tenant_id
object_id = var.principal_id
key_permissions = [
"Get",
"List",
"Update",
"Create",
"Import",
"Delete",
"Recover",
"Backup",
"Restore",
]
secret_permissions = [
"Get",
"List",
"Set",
"Delete",
"Recover",
"Backup",
"Restore",
]
certificate_permissions = [
"Get",
"List",
"Update",
"Create",
"Import",
"Delete",
"Recover",
"Backup",
"Restore",
"ManageContacts",
"ManageIssuers",
"GetIssuers",
"ListIssuers",
"SetIssuers",
"DeleteIssuers",
]
}
# Storage account key access
resource "azurerm_key_vault_access_policy" "storage-account" {
key_vault_id = azurerm_key_vault.func.id
tenant_id = azurerm_storage_account.func.identity[0].tenant_id
object_id = azurerm_storage_account.func.identity[0].principal_id
key_permissions = [
"Get",
"WrapKey",
"UnwrapKey",
]
}
# UUID for service connections
resource "random_uuid" "epconnection" {}
# Create encryption key for storage
resource "azurerm_key_vault_key" "func-storage-encryption" {
name = "${azurerm_resource_group.func.name}key"
key_vault_id = azurerm_key_vault.func.id
key_type = "RSA"
key_size = 4096
key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"]
# Make sure the ACLs are set before trying this to avoid 403
depends_on = [
azurerm_key_vault.func,
azurerm_key_vault_access_policy.deployment-account,
azurerm_key_vault_access_policy.storage-account,
]
}
# Private Endpoint for Key Vault
resource "azurerm_private_endpoint" "func-keyvault" {
name = format("%s%s", azurerm_key_vault.func.name, "privateendpoint")
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
subnet_id = azurerm_subnet.func-default.id
private_service_connection {
name = format("%s%s", azurerm_key_vault.func.name, "privateendpoint_${random_uuid.epconnection.result}")
private_connection_resource_id = azurerm_key_vault.func.id
is_manual_connection = false
subresource_names = ["vault"]
}
tags = {}
}
# Private DNS Zone for Vault
resource "azurerm_private_dns_zone" "func-keyvault" {
name = "privatelink.vaultcore.azure.net"
resource_group_name = azurerm_resource_group.func.name
}
# DNS Record for Vault
resource "azurerm_private_dns_a_record" "func-keyvault" {
name = azurerm_key_vault.func.name
zone_name = azurerm_private_dns_zone.func-keyvault.name
resource_group_name = azurerm_resource_group.func.name
ttl = 300
records = [ azurerm_private_endpoint.func-keyvault.private_service_connection[0].private_ip_address ]
}
# Create the storage account
resource "azurerm_storage_account" "func" {
name = format("%s%s%s", var.resource_group_name, var.function_app_name, var.storage_account_name)
resource_group_name = azurerm_resource_group.func.name
location = azurerm_resource_group.func.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
min_tls_version = "TLS1_2"
tags = {}
network_rules {
bypass = ["AzureServices"]
default_action = "Deny"
# Grant access from both subnets
virtual_network_subnet_ids = [
azurerm_subnet.func-default.id,
azurerm_subnet.func-apps.id,
]
# Grant access for deployment user IP address
ip_rules = [ var.my_ip ]
}
identity {
type = "SystemAssigned"
}
}
# Customer-managed encryption for Storage
resource "azurerm_storage_account_customer_managed_key" "func" {
storage_account_id = azurerm_storage_account.func.id
key_vault_id = azurerm_key_vault.func.id
key_name = azurerm_key_vault_key.func-storage-encryption.name
depends_on = [
azurerm_storage_account.func,
azurerm_key_vault_key.func-storage-encryption,
]
}
# Private Endpoint for Storage
resource "azurerm_private_endpoint" "func-storage" {
name = format("%s%s", azurerm_storage_account.func.name, "privateendpoint")
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
subnet_id = azurerm_subnet.func-default.id
private_service_connection {
name = format("%s%s", azurerm_storage_account.func.name, "privateendpoint_${random_uuid.epconnection.result}")
private_connection_resource_id = azurerm_storage_account.func.id
is_manual_connection = false
subresource_names = ["blob"]
}
tags = {}
}
# Private DNS Zone for Storage privatelink.blob.core.windows.net
resource "azurerm_private_dns_zone" "func-storage" {
name = "privatelink.blob.core.windows.net"
resource_group_name = azurerm_resource_group.func.name
}
# DNS Record for storage
resource "azurerm_private_dns_a_record" "func-storage" {
name = azurerm_storage_account.func.name
zone_name = azurerm_private_dns_zone.func-storage.name
resource_group_name = azurerm_resource_group.func.name
ttl = 300
records = [ azurerm_private_endpoint.func-storage.private_service_connection[0].private_ip_address ]
}
# Create the app service plan
resource "azurerm_app_service_plan" "func" {
name = format("%s%s%s", var.resource_group_name, var.function_app_name, var.app_service_plan_name)
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
kind = "Linux"
reserved = true
tags = {}
sku {
tier = "PremiumV2"
size = "P1v2"
}
}
# Create application insights
resource "azurerm_application_insights" "func" {
name = format("%s%s%s", var.resource_group_name, var.function_app_name, var.appinsights_name)
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
application_type = "web"
}
# Create function app
resource "azurerm_function_app" "func" {
name = format("%s%s", var.resource_group_name, var.function_app_name)
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
app_service_plan_id = azurerm_app_service_plan.func.id
storage_account_name = azurerm_storage_account.func.name
storage_account_access_key = azurerm_storage_account.func.primary_access_key
os_type = "linux"
version = "~3"
app_settings = {
APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.func.instrumentation_key
APPLICATIONINSIGHTS_CONNECTION_STRING = azurerm_application_insights.func.connection_string
FUNCTIONS_WORKER_RUNTIME = "python"
# WEBSITE_WEBDEPLOY_USE_SCM = true
# WEBSITE_START_SCM_ON_SITE_CREATION = true
}
site_config {
always_on = true
use_32_bit_worker_process = false
elastic_instance_minimum = 1
#scm_type = "ExternalGit"
}
identity {
type = "SystemAssigned"
}
# source_control {
# repo_url = var.scm_repo
# branch = var.scm_branch
# manual_integration = true
# rollback_enabled = false
# use_mercurial = false
# }
}
# Private Endpoint for Function App
resource "azurerm_private_endpoint" "func-app" {
name = format("%s%s", azurerm_function_app.func.name, "privateendpoint")
location = azurerm_resource_group.func.location
resource_group_name = azurerm_resource_group.func.name
subnet_id = azurerm_subnet.func-default.id
private_service_connection {
name = format("%s%s", azurerm_function_app.func.name, "privateendpoint-${random_uuid.epconnection.result}")
private_connection_resource_id = azurerm_function_app.func.id
is_manual_connection = false
subresource_names = ["sites"]
}
tags = {}
}
# Private DNS Zone for Function App privatelink.azurewebsites.net
resource "azurerm_private_dns_zone" "func-app" {
name = "privatelink.azurewebsites.net"
resource_group_name = azurerm_resource_group.func.name
}
# DNS Record for Function App
resource "azurerm_private_dns_a_record" "func-app" {
name = azurerm_function_app.func.name
zone_name = azurerm_private_dns_zone.func-app.name
resource_group_name = azurerm_resource_group.func.name
ttl = 300
records = [ azurerm_private_endpoint.func-app.private_service_connection[0].private_ip_address ]
}
# VNet integration for Function App
resource "azurerm_app_service_virtual_network_swift_connection" "func-app" {
app_service_id = azurerm_function_app.func.id
subnet_id = azurerm_subnet.func-apps.id
}
variable "tenant_id" {
type = string
default = ""
}
variable "subscription_id" {
type = string
default = ""
}
variable "resource_group_location" {
type = string
default = "westus"
}
# Adding my own IP for resource access
variable "my_ip" {
type = string
default = ""
}
# Deployment user ID for ACLs
variable "principal_id" {
type = string
default = ""
}
variable "resource_group_name" {
type = string
default = "func6"
}
variable "function_app_name" {
type = string
default = "app1"
}
variable "vnet_name" {
type = string
default = "vnet1"
}
variable "vault_name" {
type = string
default = "keyvault"
}
variable "storage_account_name" {
type = string
default = "storage"
}
variable "app_service_plan_name" {
type = string
default = "appplan"
}
variable "appinsights_name" {
type = string
default = "insights"
}
variable "scm_repo" {
type = string
default = ""
sensitive = true
}
variable "scm_branch" {
type = string
default = "main"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment