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
- You are onboarding any web API, function app, container app, or front end that should report traces and dependencies, and you want the connection string wired into app settings via Terraform rather than copy-paste.
- You run more than one service and need every component to share a single Log Analytics workspace so cross-service transactions correlate in the Application Map.
- Cost control matters: you want sampling and a daily cap enforced as code, not as a portal afterthought discovered when the invoice arrives.
- You want smart-detection noise (failure anomalies, slow page load, dependency latency) tuned consistently and routed to a real action group instead of the default per-resource email.
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 config — live/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 config — live/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
- Always go workspace-based and share one workspace per environment. Cross-service correlation in the Application Map and Transaction Search only works when components write to the same Log Analytics workspace; this module makes
workspace_idrequired so a classic component can never be created by accident. - Cap cost with sampling first, the daily cap second. Set
sampling_percentageto keep a representative slice on chatty services, and treatdaily_data_cap_in_gbas a hard backstop against retry storms and runaway log levels — not as your primary cost lever. - Disable local auth and prefer the connection string. Keep
local_authentication_disabled = trueand consumeconnection_string(notinstrumentation_key) so ingestion uses Entra ID; both key/string outputs are markedsensitiveso they never print in plan diffs. - Tune smart detection instead of muting it. Disable genuinely noisy rules like “Slow page load time” for backend APIs, but keep failure-anomaly detection on and point
additional_email_recipientsat a real on-call address or action group rather than the default subscription owners. - Lock down the data plane for regulated workloads. Set
internet_ingestion_enabledandinternet_query_enabledtofalseand route telemetry through an Azure Monitor Private Link Scope (AMPLS) when the environment must not touch the public internet. - Name with CAF and tag for chargeback. Stick to the
appi-<workload>-<env>-<region>convention enforced by thenamevalidation, and always passenv,workload, andmanagedBytags so the workspace’s per-component ingestion shows up cleanly in cost analysis.