IaC Azure

Terraform Module: Azure Application Insights — workspace-based APM you can stamp out per service

Quick take — A production-ready Terraform module for azurerm_application_insights: workspace-based mode, sampling, daily cap, smart-detect tuning, and a connection string output your apps consume. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "azurerm" {
  features {}
}

module "application_insights" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-azure-application-insights?ref=v1.0.0"

  name                = "..."  # Component name; must start with `appi-`.
  resource_group_name = "..."  # Resource group holding the component.
  location            = "..."  # Azure region (e.g. `centralindia`).
  workspace_id        = "..."  # Log Analytics workspace resource ID (workspace-based mo…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

Application Insights is Azure Monitor’s application performance management (APM) service. It collects distributed traces, request/dependency telemetry, exceptions, live metrics, and custom events from your apps, then powers Transaction Search, the Application Map, and Kusto queries over the requests, dependencies, traces, and exceptions tables. Since the classic ingestion model was retired, every new component must be workspace-based — telemetry actually lands in a Log Analytics workspace, and Application Insights becomes the APM lens over that data.

That single fact is why you want a module. A correct Application Insights deployment is never just one resource: it is the component bound to the right workspace, the right application_type, an ingestion sampling percentage so a chatty service does not blow your bill, a daily data cap as a hard backstop, and usually a couple of azurerm_application_insights_smart_detection_rule blocks so the default failure-anomaly emails do not spam a channel nobody reads. Wrapping all of that in terraform-module-azure-application-insights means every team stamps out an identically-governed component — same naming, same retention, same cap — instead of hand-clicking a portal resource that silently defaults to 90-day retention and no cap.

When to use it

If you only need raw platform/diagnostic logs (no APM, no traces), provision a Log Analytics workspace and diagnostic settings directly — you do not need an Application Insights component for that.

Module structure

terraform-module-azure-application-insights/
├── versions.tf
├── main.tf
├── variables.tf
└── outputs.tf
# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}
# main.tf

# Workspace-based Application Insights component.
# Classic (non-workspace) mode is retired, so workspace_id is required here.
resource "azurerm_application_insights" "this" {
  name                = var.name
  resource_group_name = var.resource_group_name
  location            = var.location
  workspace_id        = var.workspace_id
  application_type    = var.application_type

  # Ingestion sampling at the component level. 100 = keep everything.
  # Lower it for high-volume services to cap ingestion cost.
  sampling_percentage = var.sampling_percentage

  # Hard daily ingestion cap (GB). null = no cap (workspace defaults apply).
  daily_data_cap_in_gb                  = var.daily_data_cap_in_gb
  daily_data_cap_notifications_disabled = var.daily_data_cap_notifications_disabled

  retention_in_days                     = var.retention_in_days
  disable_ip_masking                    = var.disable_ip_masking
  local_authentication_disabled         = var.local_authentication_disabled
  internet_ingestion_enabled            = var.internet_ingestion_enabled
  internet_query_enabled                = var.internet_query_enabled
  force_customer_storage_for_profiler   = var.force_customer_storage_for_profiler

  tags = var.tags
}

# Tune the built-in smart-detection rules (failure anomalies, slow response,
# trace severity, etc.) and point them at a shared action group instead of
# the per-resource default email recipients.
resource "azurerm_application_insights_smart_detection_rule" "this" {
  for_each = var.smart_detection_rules

  name                               = each.key
  application_insights_id            = azurerm_application_insights.this.id
  enabled                            = each.value.enabled
  send_emails_to_subscription_owners = each.value.send_emails_to_subscription_owners
  additional_email_recipients        = each.value.additional_email_recipients
}
# variables.tf

variable "name" {
  description = "Name of the Application Insights component. Convention: appi-<workload>-<env>-<region>."
  type        = string

  validation {
    condition     = can(regex("^appi-", var.name))
    error_message = "name should start with the 'appi-' prefix per CAF abbreviations."
  }
}

variable "resource_group_name" {
  description = "Resource group that will hold the Application Insights component."
  type        = string
}

variable "location" {
  description = "Azure region for the component (e.g. centralindia, eastus)."
  type        = string
}

variable "workspace_id" {
  description = "Resource ID of the Log Analytics workspace telemetry is stored in (workspace-based mode is mandatory)."
  type        = string

  validation {
    condition     = can(regex("/providers/Microsoft.OperationalInsights/workspaces/", var.workspace_id))
    error_message = "workspace_id must be a Log Analytics workspace resource ID."
  }
}

variable "application_type" {
  description = "Telemetry shape hint used by the portal: web, java, MobileCenter, other, phone, store, or ios."
  type        = string
  default     = "web"

  validation {
    condition     = contains(["web", "java", "MobileCenter", "other", "phone", "store", "ios"], var.application_type)
    error_message = "application_type must be one of: web, java, MobileCenter, other, phone, store, ios."
  }
}

variable "sampling_percentage" {
  description = "Ingestion sampling percentage (0-100). 100 keeps all telemetry; lower it for high-volume services."
  type        = number
  default     = 100

  validation {
    condition     = var.sampling_percentage > 0 && var.sampling_percentage <= 100
    error_message = "sampling_percentage must be greater than 0 and at most 100."
  }
}

variable "daily_data_cap_in_gb" {
  description = "Hard daily ingestion cap in GB. Set null to disable the cap and inherit workspace behaviour."
  type        = number
  default     = null

  validation {
    condition     = var.daily_data_cap_in_gb == null || try(var.daily_data_cap_in_gb > 0, false)
    error_message = "daily_data_cap_in_gb must be null or a positive number."
  }
}

variable "daily_data_cap_notifications_disabled" {
  description = "Disable the email sent when the daily cap is reached."
  type        = bool
  default     = false
}

variable "retention_in_days" {
  description = "Telemetry retention in days. Allowed: 30, 60, 90, 120, 180, 270, 365, 550, 730."
  type        = number
  default     = 90

  validation {
    condition     = contains([30, 60, 90, 120, 180, 270, 365, 550, 730], var.retention_in_days)
    error_message = "retention_in_days must be one of: 30, 60, 90, 120, 180, 270, 365, 550, 730."
  }
}

variable "disable_ip_masking" {
  description = "If true, client IP addresses are stored unmasked. Keep false unless you have a privacy/legal reason to retain full IPs."
  type        = bool
  default     = false
}

variable "local_authentication_disabled" {
  description = "Disable instrumentation-key (local) auth and force Entra ID auth for ingestion. Recommended true for new workloads."
  type        = bool
  default     = true
}

variable "internet_ingestion_enabled" {
  description = "Allow telemetry ingestion from the public internet. Set false when ingesting only via Private Link / AMPLS."
  type        = bool
  default     = true
}

variable "internet_query_enabled" {
  description = "Allow querying telemetry from the public internet. Set false when queries must traverse Private Link / AMPLS."
  type        = bool
  default     = true
}

variable "force_customer_storage_for_profiler" {
  description = "Force the Profiler/Snapshot Debugger to use a customer-managed storage account."
  type        = bool
  default     = false
}

variable "smart_detection_rules" {
  description = "Map of smart-detection rules to manage, keyed by the exact rule name. Used to silence noisy defaults or redirect alerts to an action group."
  type = map(object({
    enabled                            = optional(bool, true)
    send_emails_to_subscription_owners = optional(bool, true)
    additional_email_recipients        = optional(list(string), [])
  }))
  default = {}
}

variable "tags" {
  description = "Tags applied to the Application Insights component."
  type        = map(string)
  default     = {}
}
# outputs.tf

output "id" {
  description = "Resource ID of the Application Insights component."
  value       = azurerm_application_insights.this.id
}

output "name" {
  description = "Name of the Application Insights component."
  value       = azurerm_application_insights.this.name
}

output "app_id" {
  description = "The Application ID (used by the Application Insights REST API)."
  value       = azurerm_application_insights.this.app_id
}

output "instrumentation_key" {
  description = "Instrumentation key. Legacy; prefer connection_string. Marked sensitive."
  value       = azurerm_application_insights.this.instrumentation_key
  sensitive   = true
}

output "connection_string" {
  description = "Connection string for the SDK / OpenTelemetry exporter. Wire this into app settings. Marked sensitive."
  value       = azurerm_application_insights.this.connection_string
  sensitive   = true
}

How to use it

# A shared workspace already exists; every component points at it so
# cross-service transactions correlate in the Application Map.
data "azurerm_log_analytics_workspace" "shared" {
  name                = "log-platform-prod-cin"
  resource_group_name = "rg-observability-prod"
}

module "application_insights" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-azure-application-insights?ref=v1.0.0"

  name                = "appi-orders-api-prod-cin"
  resource_group_name = "rg-orders-prod"
  location            = "centralindia"
  workspace_id        = data.azurerm_log_analytics_workspace.shared.id

  application_type    = "web"
  sampling_percentage = 50   # high-volume API: keep half the telemetry
  retention_in_days   = 90

  # Backstop so a retry storm cannot run up the bill.
  daily_data_cap_in_gb          = 5
  local_authentication_disabled = true

  # Quiet the noisy slow-page rule; route failure anomalies to the on-call group.
  smart_detection_rules = {
    "Slow page load time" = {
      enabled = false
    }
    "Failure Anomalies - failureAnomaliesRule" = {
      enabled                            = true
      send_emails_to_subscription_owners = false
      additional_email_recipients        = ["orders-oncall@teknohut.com"]
    }
  }

  tags = {
    env       = "prod"
    workload  = "orders-api"
    managedBy = "terraform"
  }
}

# Downstream: inject the connection string into the App Service so the
# OpenTelemetry / App Insights SDK auto-instruments on startup.
resource "azurerm_linux_web_app" "orders_api" {
  name                = "app-orders-api-prod-cin"
  resource_group_name = "rg-orders-prod"
  location            = "centralindia"
  service_plan_id     = azurerm_service_plan.orders.id

  site_config {}

  app_settings = {
    "APPLICATIONINSIGHTS_CONNECTION_STRING" = module.application_insights.connection_string
    "ApplicationInsightsAgent_EXTENSION_VERSION" = "~3"
  }
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "azurerm"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...azurerm state bucket/container + key per path...
  }
}

2. Module configlive/prod/application_insights/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-azure-application-insights?ref=v1.0.0"
}

inputs = {
  name = "..."
  resource_group_name = "..."
  location = "..."
  workspace_id = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/application_insights && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
name string Yes Component name; must start with appi-.
resource_group_name string Yes Resource group holding the component.
location string Yes Azure region (e.g. centralindia).
workspace_id string Yes Log Analytics workspace resource ID (workspace-based mode is mandatory).
application_type string "web" No Telemetry hint: web, java, MobileCenter, other, phone, store, ios.
sampling_percentage number 100 No Ingestion sampling, 0 < x <= 100.
daily_data_cap_in_gb number null No Hard daily ingestion cap in GB; null disables it.
daily_data_cap_notifications_disabled bool false No Suppress the cap-reached email.
retention_in_days number 90 No One of 30/60/90/120/180/270/365/550/730.
disable_ip_masking bool false No Store client IPs unmasked when true.
local_authentication_disabled bool true No Force Entra ID auth for ingestion; disables instrumentation-key auth.
internet_ingestion_enabled bool true No Allow public-internet ingestion (set false for Private Link / AMPLS).
internet_query_enabled bool true No Allow public-internet querying (set false for Private Link / AMPLS).
force_customer_storage_for_profiler bool false No Force customer-managed storage for Profiler/Snapshot Debugger.
smart_detection_rules map(object) {} No Smart-detection rules keyed by exact rule name; tune enabled, owner emails, and recipients.
tags map(string) {} No Tags applied to the component.

Outputs

Name Description
id Resource ID of the Application Insights component.
name Component name.
app_id Application ID used by the Application Insights REST API.
instrumentation_key Legacy instrumentation key (sensitive); prefer the connection string.
connection_string Connection string for the SDK / OpenTelemetry exporter (sensitive).

Enterprise scenario

A retail platform runs roughly forty microservices across App Service and AKS, all reporting to one log-platform-prod-cin workspace. The platform team publishes this module at v1.0.0 and requires every service repo to consume it, so each component lands with the appi- naming convention, a 5 GB daily cap, and 90-day retention by default. When a Black Friday retry storm hammered the payments service, the cap stopped a single component from doubling the monthly ingestion bill, and because every team had wired connection_string through the same module output, the Application Map stitched the failing payment-to-inventory call chain together end-to-end without anyone touching the portal.

Best practices

TerraformAzureApplication InsightsModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading