Deploying Azure App Service with Terraform

Learn how to set up and manage Azure App Service for web applications using Terraform, including scaling, monitoring, and best practices

Azure App Service is a fully managed platform for building, deploying, and scaling web apps. This guide demonstrates how to set up and manage Azure App Service using Terraform.

Video Tutorial

Learn more about managing Azure App Service with Terraform in this comprehensive video tutorial:

Prerequisites

  • Azure CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • Resource group created
  • Application code ready for deployment

Project Structure

terraform-azure-appservice/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│   └── appservice/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── app/
    └── src/

App Service Configuration

Create modules/appservice/main.tf:

# App Service Plan
resource "azurerm_service_plan" "main" {
  name                = "${var.project_name}-plan"
  location            = var.location
  resource_group_name = var.resource_group_name
  os_type             = "Linux"
  sku_name            = var.sku_name

  tags = var.tags
}

# App Service
resource "azurerm_linux_web_app" "main" {
  name                = "${var.project_name}-app"
  location            = var.location
  resource_group_name = var.resource_group_name
  service_plan_id     = azurerm_service_plan.main.id

  site_config {
    always_on        = true
    http2_enabled    = true
    
    application_stack {
      node_version = "18-lts"  # Or your preferred runtime
    }

    ip_restriction {
      ip_address = var.allowed_ip_range
      priority   = 100
      action     = "Allow"
    }

    cors {
      allowed_origins = var.cors_allowed_origins
    }
  }

  app_settings = {
    "WEBSITE_NODE_DEFAULT_VERSION" = "~18"
    "WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
    "DOCKER_REGISTRY_SERVER_URL"    = var.docker_registry_url
    "DOCKER_REGISTRY_SERVER_USERNAME" = var.docker_registry_username
    "DOCKER_REGISTRY_SERVER_PASSWORD" = var.docker_registry_password
    "APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.main.instrumentation_key
  }

  identity {
    type = "SystemAssigned"
  }

  auth_settings {
    enabled                       = true
    default_provider             = "AzureActiveDirectory"
    unauthenticated_client_action = "RedirectToLoginPage"

    active_directory {
      client_id         = var.aad_client_id
      client_secret     = var.aad_client_secret
      allowed_audiences = [var.app_url]
    }
  }

  logs {
    http_logs {
      file_system {
        retention_in_days = 7
        retention_in_mb   = 35
      }
    }

    application_logs {
      file_system_level = "Information"
    }
  }

  tags = var.tags
}

# Application Insights
resource "azurerm_application_insights" "main" {
  name                = "${var.project_name}-appinsights"
  location            = var.location
  resource_group_name = var.resource_group_name
  application_type    = "web"

  tags = var.tags
}

# Custom Domain
resource "azurerm_app_service_custom_hostname_binding" "main" {
  hostname            = var.custom_domain
  app_service_name    = azurerm_linux_web_app.main.name
  resource_group_name = var.resource_group_name

  depends_on = [
    azurerm_dns_txt_record.verification
  ]
}

# SSL Certificate
resource "azurerm_app_service_certificate" "main" {
  name                = "${var.project_name}-cert"
  resource_group_name = var.resource_group_name
  location            = var.location
  pfx_blob           = filebase64(var.ssl_certificate_path)
  password           = var.ssl_certificate_password
}

# SSL Binding
resource "azurerm_app_service_certificate_binding" "main" {
  hostname_binding_id = azurerm_app_service_custom_hostname_binding.main.id
  certificate_id      = azurerm_app_service_certificate.main.id
  ssl_state          = "SniEnabled"
}

Scaling Configuration

  1. Auto-scaling Rules
resource "azurerm_monitor_autoscale_setting" "main" {
  name                = "${var.project_name}-autoscale"
  resource_group_name = var.resource_group_name
  location            = var.location
  target_resource_id  = azurerm_service_plan.main.id

  profile {
    name = "defaultProfile"

    capacity {
      default = var.default_instance_count
      minimum = var.min_instance_count
      maximum = var.max_instance_count
    }

    rule {
      metric_trigger {
        metric_name        = "CpuPercentage"
        metric_resource_id = azurerm_service_plan.main.id
        time_grain        = "PT1M"
        statistic         = "Average"
        time_window      = "PT5M"
        time_aggregation = "Average"
        operator         = "GreaterThan"
        threshold        = 75
      }

      scale_action {
        direction = "Increase"
        type      = "ChangeCount"
        value     = "1"
        cooldown  = "PT5M"
      }
    }

    rule {
      metric_trigger {
        metric_name        = "CpuPercentage"
        metric_resource_id = azurerm_service_plan.main.id
        time_grain        = "PT1M"
        statistic         = "Average"
        time_window      = "PT5M"
        time_aggregation = "Average"
        operator         = "LessThan"
        threshold        = 25
      }

      scale_action {
        direction = "Decrease"
        type      = "ChangeCount"
        value     = "1"
        cooldown  = "PT5M"
      }
    }
  }
}

Network Configuration

  1. Virtual Network Integration
resource "azurerm_virtual_network" "main" {
  name                = "${var.project_name}-vnet"
  location            = var.location
  resource_group_name = var.resource_group_name
  address_space       = ["10.0.0.0/16"]

  tags = var.tags
}

# The inline subnet block on azurerm_virtual_network is deprecated - use a
# standalone azurerm_subnet resource instead
resource "azurerm_subnet" "integration" {
  name                 = "integration-subnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.1.0/24"]

  delegation {
    name = "appservice"

    service_delegation {
      name    = "Microsoft.Web/serverFarms"
      actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
    }
  }
}

resource "azurerm_app_service_virtual_network_swift_connection" "main" {
  app_service_id = azurerm_linux_web_app.main.id
  subnet_id      = azurerm_subnet.integration.id
}

Monitoring Configuration

  1. Diagnostic Settings
resource "azurerm_monitor_diagnostic_setting" "app" {
  name                       = "${var.project_name}-diag"
  target_resource_id        = azurerm_linux_web_app.main.id
  log_analytics_workspace_id = var.log_analytics_workspace_id

  log {
    category = "AppServiceHTTPLogs"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }

  log {
    category = "AppServiceConsoleLogs"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }

  metric {
    category = "AllMetrics"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }
}

resource "azurerm_monitor_metric_alert" "response_time" {
  name                = "${var.project_name}-response-time-alert"
  resource_group_name = var.resource_group_name
  scopes              = [azurerm_linux_web_app.main.id]
  description         = "Alert when response time is high"

  criteria {
    metric_namespace = "Microsoft.Web/sites"
    metric_name      = "HttpResponseTime"
    aggregation      = "Average"
    operator         = "GreaterThan"
    threshold        = 5
  }

  action {
    action_group_id = var.action_group_id
  }
}

Deployment Slots

  1. Staging Slot
resource "azurerm_linux_web_app_slot" "staging" {
  name           = "staging"
  app_service_id = azurerm_linux_web_app.main.id

  site_config {
    always_on     = true
    http2_enabled = true

    application_stack {
      node_version = "18-lts"
    }
  }

  app_settings = merge(
    azurerm_linux_web_app.main.app_settings,
    {
      "ASPNETCORE_ENVIRONMENT" = "Staging"
    }
  )

  identity {
    type = "SystemAssigned"
  }

  tags = var.tags
}

resource "azurerm_app_service_slot_virtual_network_swift_connection" "staging" {
  slot_name      = azurerm_linux_web_app_slot.staging.name
  app_service_id = azurerm_linux_web_app.main.id
  subnet_id      = azurerm_subnet.integration.id
}

Best Practices

  1. Security

    • Enable authentication
    • Use managed identities
    • Implement IP restrictions
    • Enable HTTPS only
  2. Performance

    • Enable auto-scaling
    • Configure staging slots
    • Use premium tier for production
    • Enable HTTP/2
  3. Monitoring

    • Enable Application Insights
    • Configure alerts
    • Set up logging
    • Monitor performance
  4. Cost Optimization

    • Right-size app service plan
    • Use auto-scaling
    • Monitor usage
    • Consider reserved instances

Advanced Features

  1. WebJobs

There’s no dedicated Terraform resource for WebJobs - they’re deployed as part of the app’s content (a zip package placed under app_data/jobs/continuous/ or app_data/jobs/triggered/), not modeled as a separate Terraform resource. Deploy the WebJob’s files alongside your app code through your normal deployment pipeline (az webapp deploy, a GitHub Action, etc.) rather than through Terraform.

  1. Hybrid Connections
resource "azurerm_app_service_hybrid_connection" "example" {
  app_service_name    = azurerm_linux_web_app.main.name
  resource_group_name = var.resource_group_name
  relay_id           = azurerm_relay_hybrid_connection.example.id
  hostname           = "internal.example.com"
  port               = 3306
}

Conclusion

You’ve learned how to set up and manage Azure App Service using Terraform. This setup provides:

  • Secure web app deployment
  • Auto-scaling capabilities
  • Monitoring and alerts
  • Custom domain and SSL

Remember to:

  • Monitor performance
  • Implement security best practices
  • Optimize costs
  • Use deployment slots for zero-downtime deployments