Creating a web app - Terraform

The preferred structure for a Web App on Azure is for the configuration to be held in an Azure App Configuration instance, with secrets in an Azure Key Vault. Visibility of performance and errors in the app is provided by App Insights. Application logs should be saved to blob storage.

Depends upon

For the standard configuration of a web app, you will also need the following resources:

  • App Service Plan
  • App Service Certificate (if using a Cloudflare Origin cert) - tbc - what happens if using a Microsoft issued certificates
  • App Insights
  • App Configuration
  • Key Vault
  • Blob Storage (if saving logs to storage)

Configuration for custom domain

Two main options affect the setup of a custom domain:

  • A Cloudflare Origin certificate or a third-party certificate has been uploaded into key vault (preferred)
  • Use of Microsoft issued TLS certificates

Terraform/Azure resources to be configured

  • Azure Windows Web App (or Linux Web App)
    • Site configuration:
      • Application stack
    • Identity
      • SystemAssigned
    • Connection String - assuming site uses DB
    • Logs
  • Azure app service connection
    • App Configuration
    • Azure SQL
    • Blob storage (if the app needs access to blob storage)
    • Key vault
  • Azure app service custom hostname binding
  • Azure app service certificate binding

Service connections

The service connections on the web app ensures that appropriate permissions and firewall connections are created for accessing the downstream resources. These setup access to use the System Managed Identities which is Microsoft’s preferred (& most secure) way of connecting to the resources.

Please note: for Framework v4.x apps you need to ensure your app is using Microsoft.Data.SqlClient - older apps will have been using System.Data.SqlClient. The older libraries don’t have support for modern authentication methods.

Terraform script

# Admin API
resource "azurerm_windows_web_app" "app_adminapi" {
  name                = var.app_api_name
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  service_plan_id     = azurerm_service_plan.appserviceplan.id
  site_config {
    application_stack {
      current_stack  = "dotnet"
      dotnet_version = "v8.0"
    }
      always_on      = true
  }
  identity {
    type = "SystemAssigned"
  }
  app_settings = {
    "WEBSITE_ENABLE_SYNC_UPDATE_SITE" : "true"
    "AZURE_APPCONFIGURATION_ENDPOINT" = azurerm_app_configuration.appconfig.endpoint
    "APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.appinsights.instrumentation_key
    "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.appinsights.connection_string
    "ApplicationInsightsAgent_EXTENSION_VERSION" = "~2"
  }
  depends_on = [ azurerm_application_insights.appinsights ]
  connection_string {
    name  = "iscquare"
    type  = "SQLAzure"
    value = "Server=tcp:${var.sql_server_name}.database.windows.net,1433;Initial Catalog=${azurerm_mssql_database.sqldb_main.name};Authentication=Active Directory Managed Identity;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
  }
  logs {
    detailed_error_messages = false
    failed_request_tracing  = false
    application_logs {
      file_system_level = "Error"
    }
    http_logs {
      file_system {
        retention_in_days = 7
        retention_in_mb   = 35
      }
    }
  }
}
# Create service connector to app config
resource "azurerm_app_service_connection" "api_appconfig_connection" {
  name               = "api_appconfig_connection"
  app_service_id     = azurerm_windows_web_app.app_adminapi.id
  target_resource_id = azurerm_app_configuration.appconfig.id
  authentication {
    type = "systemAssignedIdentity"
  }
}
# Create service connector to Azure SQL Database
resource "azurerm_app_service_connection" "api_sqldb_connection" {
  name               = "api_sqldb_connection"
  app_service_id     = azurerm_windows_web_app.app_adminapi.id
  target_resource_id = azurerm_mssql_database.sqldb_main.id
  authentication {
    type = "systemAssignedIdentity"
  }
}
# Create service connector to Azure Blob Storage
resource "azurerm_app_service_connection" "api_storage_connection" {
  name               = "api_storage_connection"
  app_service_id     = azurerm_windows_web_app.app_adminapi.id
  target_resource_id = azurerm_storage_account.storage.id
  authentication {
    type = "systemAssignedIdentity"
  }
}
# Create service connector to Key Vault
resource "azurerm_app_service_connection" "api_keyvault_connection" {
  name               = "api_keyvault_connection"
  app_service_id     = azurerm_windows_web_app.app_adminapi.id
  target_resource_id = azurerm_key_vault.keyvault.id
  authentication {
    type = "systemAssignedIdentity"
  }
}
# Assign api-uat custom domain to the admin API
resource "azurerm_app_service_custom_hostname_binding" "adminapi_custom_domain" {
  hostname            = var.app_api_hostname
  app_service_name    = azurerm_windows_web_app.app_adminapi.name
  resource_group_name = azurerm_resource_group.rg.name
}
# bind certificate to admin api
resource "azurerm_app_service_certificate_binding" "adminapi_cert_binding" {
  hostname_binding_id = azurerm_app_service_custom_hostname_binding.adminapi_custom_domain.id
  certificate_id      = azurerm_app_service_certificate.iscquare_net_cert.id
  ssl_state           = "SniEnabled"
}