Last active
September 3, 2021 22:41
-
-
Save ChrisRomp/c5b97465c915359829ac1b35b6b403ca to your computer and use it in GitHub Desktop.
Terraform Function App with Encrypted Storage and VNet Isolation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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