Azure App Service with Autoscaling deploy with Terraform
Prerequisites:
Terraform 1.5+, Azure CLI, and an Azure subscription with permission to create these resources. Sign in and select the intended subscription before applying it.
Modularization in Terraform
resource "random_string" "suffix" {
length = 6
lower = true
upper = false
numeric = true
special = false
}
locals {
resource_suffix = coalesce(var.resource_suffix, random_string.suffix.result)
}
resource "azurerm_resource_group" "app" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_log_analytics_workspace" "app" {
name = "law-${var.app_name_prefix}-${local.resource_suffix}"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
sku = "PerGB2018"
retention_in_days = 30
}
resource "azurerm_application_insights" "app" {
name = "appi-${var.app_name_prefix}-${local.resource_suffix}"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
application_type = "web"
workspace_id = azurerm_log_analytics_workspace.app.id
retention_in_days = 30
}
resource "azurerm_service_plan" "app" {
name = "asp-${var.app_name_prefix}-${local.resource_suffix}"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
os_type = "Linux"
sku_name = var.service_plan_sku
}
data "archive_file" "webapp" {
type = "zip"
source_dir = var.app_source_dir
output_path = "${path.root}/.terraform/${var.resource_group_name}-${var.app_name_prefix}-webapp.zip"
}
resource "azurerm_linux_web_app" "app" {
name = "${var.web_app_name_prefix}-${local.resource_suffix}"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
service_plan_id = azurerm_service_plan.app.id
https_only = true
zip_deploy_file = data.archive_file.webapp.output_path
app_settings = {
APPLICATIONINSIGHTS_CONNECTION_STRING = azurerm_application_insights.app.connection_string
APP_PACKAGE_HASH = data.archive_file.webapp.output_base64sha256
SCM_DO_BUILD_DURING_DEPLOYMENT = "true"
}
site_config {
always_on = true
app_command_line = "npm start"
health_check_path = "/health"
health_check_eviction_time_in_min = 10
application_stack {
node_version = "20-lts"
}
}
logs {
application_logs {
file_system_level = "Information"
}
}
}
resource "azurerm_monitor_autoscale_setting" "app" {
name = "autoscale-${var.app_name_prefix}-${local.resource_suffix}"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
target_resource_id = azurerm_service_plan.app.id
profile {
name = "cpu-based-autoscale"
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.app.id
time_grain = "PT1M"
statistic = "Average"
time_window = "PT10M"
time_aggregation = "Average"
operator = "GreaterThan"
threshold = 70
}
scale_action {
direction = "Increase"
type = "ChangeCount"
value = "1"
cooldown = "PT5M"
}
}
rule {
metric_trigger {
metric_name = "CpuPercentage"
metric_resource_id = azurerm_service_plan.app.id
time_grain = "PT1M"
statistic = "Average"
time_window = "PT20M"
time_aggregation = "Average"
operator = "LessThan"
threshold = 30
}
scale_action {
direction = "Decrease"
type = "ChangeCount"
value = "1"
cooldown = "PT10M"
}
}
}
}
Variables for App Service
Add Application Insights and Log Analytics so you can see what scaling is reacting to.
variable "location" {
description = "Azure region for the resources."
type = string
}
variable "resource_group_name" {
description = "Name of the resource group to create."
type = string
}
variable "app_source_dir" {
description = "Path to the directory containing the web app source."
type = string
}
variable "app_name_prefix" {
description = "Prefix used for the App Service Plan and monitoring resource names."
type = string
validation {
condition = can(regex("^[A-Za-z0-9-]+$", var.app_name_prefix))
error_message = "app_name_prefix must contain only letters, numbers, and hyphens."
}
}
variable "web_app_name_prefix" {
description = "Prefix used for the globally unique Web App name."
type = string
validation {
condition = can(regex("^[A-Za-z0-9-]+$", var.web_app_name_prefix))
error_message = "web_app_name_prefix must contain only letters, numbers, and hyphens."
}
}
variable "resource_suffix" {
description = "Optional six-character suffix for resource names; leave null to generate one."
type = string
default = null
nullable = true
validation {
condition = var.resource_suffix == null || can(regex("^[a-z0-9]{6}$", var.resource_suffix))
error_message = "resource_suffix must be null or exactly six lowercase letters or digits."
}
}
variable "service_plan_sku" {
description = "SKU for the Linux App Service Plan."
type = string
default = "S1"
}
variable "min_instance_count" {
description = "Minimum number of App Service Plan instances."
type = number
default = 1
}
variable "default_instance_count" {
description = "Default number of App Service Plan instances."
type = number
default = 1
}
variable "max_instance_count" {
description = "Maximum number of App Service Plan instances."
type = number
default = 3
validation {
condition = (
var.min_instance_count >= 1 &&
var.min_instance_count <= var.default_instance_count &&
var.default_instance_count <= var.max_instance_count
)
error_message = "Instance counts must satisfy 1 \<= minimum \<= default \<= maximum."
}
}
Define the autoscale setting with a minimum, default and maximum instance count, plus scale-out and scale-in rules based on metrics (CPU, memory, or HTTP queue length). terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
}
archive = {
source = "hashicorp/archive"
}
random = {
source = "hashicorp/random"
}
}
}
Deploy the application code (zip deploy, GitHub Actions, or a container image).
Web App Service output
output "web_app_url" {
description = "HTTPS URL of the web app."
value = "https://${azurerm_linux_web_app.app.default_hostname}"
}
output "web_app_name" {
description = "Globally unique name of the deployed web app."
value = azurerm_linux_web_app.app.name
}
output "app_insights_name" {
description = "Application Insights resource name."
value = azurerm_application_insights.app.name
}
output "log_analytics_workspace_name" {
description = "Log Analytics workspace name."
value = azurerm_log_analytics_workspace.app.name
}
output "app_service_plan_id" {
description = "App Service Plan ID targeted by the autoscale setting."
value = azurerm_service_plan.app.id
}
Terraform Root directory code
1. main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
archive = {
source = "hashicorp/archive"
version = "~> 2.7"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "azurerm" {
features {}
}
module "app_service" {
source = "./modules/app_service"
location = var.location
resource_group_name = var.resource_group_name
app_source_dir = "${path.root}/app"
app_name_prefix = var.app_name_prefix
web_app_name_prefix = var.web_app_name_prefix
resource_suffix = var.resource_suffix
service_plan_sku = var.service_plan_sku
min_instance_count = var.min_instance_count
default_instance_count = var.default_instance_count
max_instance_count = var.max_instance_count
}
2. variables.tf
variable "location" {
description = "Azure region for the resources."
type = string
default = "southindia"
}
variable "resource_group_name" {
description = "Name of the resource group to create."
type = string
default = "rg-appservice-autoscale-demo"
}
variable "app_name_prefix" {
description = "Prefix used for the App Service Plan and monitoring resource names."
type = string
default = "appservice"
}
variable "web_app_name_prefix" {
description = "Prefix used for the globally unique Web App name."
type = string
default = "web"
}
variable "resource_suffix" {
description = "Optional six-character suffix for resource names; leave null to generate one."
type = string
default = "9equss"
nullable = true
}
variable "service_plan_sku" {
description = "SKU for the Linux App Service Plan."
type = string
default = "S1"
}
variable "min_instance_count" {
description = "Minimum number of App Service Plan instances."
type = number
default = 1
}
variable "default_instance_count" {
description = "Default number of App Service Plan instances."
type = number
default = 1
}
variable "max_instance_count" {
description = "Maximum number of App Service Plan instances."
type = number
default = 3
}
3. output.tf
output "web_app_url" {
description = "HTTPS URL of the sample web app."
value = module.app_service.web_app_url
}
output "web_app_name" {
description = "Globally unique name of the deployed web app."
value = module.app_service.web_app_name
}
output "app_insights_name" {
description = "Application Insights resource for application telemetry."
value = module.app_service.app_insights_name
}
output "log_analytics_workspace_name" {
description = "Workspace receiving Application Insights telemetry."
value = module.app_service.log_analytics_workspace_name
}
output "app_service_plan_id" {
description = "App Service Plan ID targeted by the autoscale setting."
value = module.app_service.app_service_plan_id
}
Create Node.js application
Simple web application with Node.js with port 8080 as exposing.
const { useAzureMonitor } = require("@azure/monitor-opentelemetry");
const http = require("node:http");
useAzureMonitor();
const server = http.createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200, { "content-type": "text/plain" });
response.end("Super healthy");
return;
}
if (request.url === "/work") {
const finishAt = Date.now() + 500;
let value = 0;
while (Date.now() < finishAt) {
value = Math.sqrt(value + Math.random());
}
response.writeHead(200, { "content-type": "text/plain" });
response.end(`Work completed: ${value.toFixed(2)}`);
return;
}
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
response.end(
"Vybhava App Service autoscale sample " +
"Vybhava App Service autoscale sample
" +
"Application Insights is collecting request telemetry.
" +
"Visit /work to generate a brief CPU workload.
",
);
});
const port = Number(process.env.PORT || 8080);
server.listen(port);
Meta data about the app can be passed with JSON file.
package.json
{
"name": "appservice-autoscale-sample",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@azure/monitor-opentelemetry": "^1.8.0"
}
}
Leave a wide gap between the scale-out threshold (70) and the scale-in threshold (30), and use a longer scale-in window. Otherwise the plan flaps between instance counts.
![]() |
| Test the Web App from Web browser |
Install Load test CLI tool 'hey'
hey is a single Go binary, so you only need to get it onto your machine and onto your PATH. Most of our Indian learners uses Windows system, so let's see command set that works on Powershell.winget install GoLang.Go
# close and reopen PowerShell, then:
go install github.com/rakyll/hey@latest
# add Go's bin folder to PATH permanently
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$(go env GOPATH)\bin", "User")
# check the installation
hey -h
Deploy and test application
terraform init terraform apply # Deploy a zip of your app az webapp deploy --resource-group rg-webapp-demo \ --name--src-path app.zip --type zip # Generate load (for example with hey or ab) hey -z 5m -c 50 https:// .azurewebsites.net # Watch instance count az monitor autoscale show -g rg-webapp-demo -n autoscale-webapp-demo
az login az account set --subscription "" terraform init terraform plan terraform apply
The module exports the Web App URL and name, Application Insights and Log Analytics names, and App Service Plan ID. Each instance packages the supplied app source directory as a ZIP for deployment. Use distinct resource groups and name prefixes for separate module instances.
Application Insights uses the Log Analytics workspace for request and dependency telemetry. To inspect scale activity, open the App Service Plan's Metrics in the Azure portal and view CPU Percentage; review scale operations under Autoscale or the resource group's **Activity log**.
This example uses the App Service public endpoint directly; it does not create Azure Front Door or a separate load balancer. Standard S1 App Service Plan instances incur ongoing Azure charges, so run `terraform destroy` when the environment is no longer needed.

Comments