Wires an Azure App Service to a backend target via Service Connector, for
hashicorp/azurerm ~> 4.0, with managed-identity authentication as the default path.
- Creates an
azurerm_app_service_connection(the keystonethis) — a Service Connector link from a web app to a target backend (Storage, SQL, Key Vault, Service Bus, Cosmos DB, Event Hubs, and more). - Configures authentication: system- or user-assigned managed identity (preferred), service principal (secret/certificate), or a plain secret.
- Optionally routes traffic over a VNet solution (
serviceEndpoint/privateLink) and persists secrets to a Key Vaultsecret_store. - Emits the connection
id,name, andtarget_resource_id; never emits a secret.
💡 Why it matters: Service Connector is where an app's backend credentials usually live. This module makes managed identity the default so the common path involves no secret at all, and marks any unavoidable secret/certificate
sensitive.
If this module saves you time:
- ⭐ Star the repository
- 💼 Connect on LinkedIn
- ☕ Buy me a coffee
flowchart LR
web["terraform-azurerm-linux-web-app / windows-web-app"]
tgt["target service: Storage / SQL / Key Vault / Service Bus / Cosmos"]
kv["terraform-azurerm-key-vault"]
sib["terraform-azurerm-function-app-connection (sibling)"]
this["terraform-azurerm-app-service-connection"]
conn["azurerm_app_service_connection"]
web -->|"app_service_id"| this
tgt -->|"target_resource_id"| this
kv -.->|"secret_store.key_vault_id (optional)"| this
this -->|"creates"| conn
sib -.->|"same Service Connector family"| conn
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class this me;
class conn target;
class web,tgt,kv,sib ext;
flowchart TB
subgraph inputs["Inputs"]
app["app_service_id"]
target["target_resource_id"]
auth["authentication {type, secret, certificate}"]
ss["secret_store {key_vault_id} (optional)"]
end
this["azurerm_app_service_connection.this"]
out["Outputs: id, name, target_resource_id"]
app -->|"source web app"| this
target -->|"backend"| this
auth -->|"managed identity preferred"| this
ss -.->|"persist secret in Key Vault"| this
this --> out
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class this target;
class out me;
class app,target,auth,ss ext;
Resource inventory
| Resource | Role | Cardinality |
|---|---|---|
azurerm_app_service_connection |
keystone this |
single |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azurerm provider | ~> 4.0 |
| Provider block | None — the caller configures provider "azurerm" { features {} }, auth, and subscription |
Schema notes that bite (verified against the live schema):
- Force-new:
name,app_service_id,target_resource_id, andauthentication.typeare immutable — changing any replaces the connection. - No tags: the resource does not support
tags; the universal tail here istimeoutsonly. authentication.secretandauthentication.certificateare provider-sensitive and wrappedsensitive()here.authenticationis required (a connection must say how it authenticates). Identity types need the identity configured on the App Service.- The target is any supported Service Connector backend (
target_resource_id); the connection's identity still needs a data-plane role on that target, granted separately.
- Website Contributor (or equivalent) on the App Service.
- The connection's identity/principal needs a data-plane role on the target (e.g. Storage Blob Data Contributor, Key Vault Secrets User) — grant it separately.
- The
Microsoft.ServiceLinkerresource provider registered on the subscription. - An existing App Service and an existing target resource.
- For identity-based auth: a system- or user-assigned identity configured on the App Service.
terraform-azurerm-app-service-connection/
├── providers.tf # required_version + azurerm ~> 4.0 pin; no provider block
├── variables.tf # keystone inputs, authentication + secret_store blocks, timeouts (no tags)
├── main.tf # azurerm_app_service_connection.this
├── outputs.tf # id first, then name, target_resource_id
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore
The smallest real call uses the app's system-assigned identity — no secret:
module "app_conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "storage"
app_service_id = var.web_app_id
target_resource_id = var.storage_account_id
authentication = { type = "systemAssignedIdentity" }
}ℹ️ The caller configures
provider "azurerm" { features {} }, authentication, and the subscription — this module declares none of them. The system-assigned identity must be enabled on the App Service.
Consumes
| Input | Type | Source module |
|---|---|---|
app_service_id |
string |
terraform-azurerm-linux-web-app / windows-web-app (id) |
target_resource_id |
string |
the target service module (id) |
secret_store.key_vault_id |
string |
terraform-azurerm-key-vault (id) |
Emits
| Output | Description |
|---|---|
id |
Connection Resource ID (first) |
name |
Connection name |
target_resource_id |
Bound target resource ID |
app_service_id |
The app this connection mutates |
target_resource_id / target_resource_type |
What it binds to |
authentication_type |
Unwrapped with nonsensitive() on purpose |
uses_managed_identity |
No credential at all |
supplies_a_credential |
Presence only, never the value |
credential_is_persisted_in_key_vault |
A secret store is configured |
credential_supplied_without_a_secret_store |
Credential lives in the app's settings |
secret_store_key_vault_id |
The vault, or null |
client_type / vnet_solution / traffic_avoids_the_public_path |
Config shape and networking |
settings_written_to_the_app_are_not_owned_here |
Constant: this module mutates the App Service |
connection_may_create_a_role_assignment_on_the_target |
Constant: and it is unmanaged |
secrets_supplied_here_are_stored_in_state |
Constant: sensitive redacts plan, not state |
this_module_emits_no_credential |
Constant: presence flags only |
the_target_and_the_identity_are_not_verified |
Constant: almost nothing is checked |
1 · System-assigned identity to Storage (recommended)
module "conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "storage"
app_service_id = var.web_app_id
target_resource_id = var.storage_account_id
authentication = { type = "systemAssignedIdentity" }
}🔒 No secret is handled at all — the app's identity authenticates to the target.
2 · User-assigned identity
authentication = {
type = "userAssignedIdentity"
client_id = var.uami_client_id
subscription_id = var.subscription_id # pair client_id + subscription_id
}3 · Service principal with secret (+ Key Vault store)
module "conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "sql"
app_service_id = var.web_app_id
target_resource_id = var.sql_server_id
authentication = {
type = "servicePrincipalSecret"
client_id = var.sp_client_id
principal_id = var.sp_principal_id
secret = var.sp_secret # sensitive; provision out of band
}
secret_store = { key_vault_id = var.key_vault_id } # persist the secret in Key Vault
}🔒
secretis wrappedsensitive;secret_storekeeps it in Key Vault rather than the connection config.
4 · Service principal with certificate
authentication = {
type = "servicePrincipalCertificate"
client_id = var.sp_client_id
principal_id = var.sp_principal_id
certificate = var.sp_certificate # sensitive
}5 · Secret (username + key) auth
authentication = {
type = "secret"
name = var.db_user
secret = var.db_password # name and secret both set (or both omitted)
}
⚠️ Prefer a managed identity where the target supports it; secret auth stores credentials.
6 · Target: Key Vault
module "conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "kv"
app_service_id = var.web_app_id
target_resource_id = var.key_vault_id
authentication = { type = "systemAssignedIdentity" }
}7 · Target: SQL database
target_resource_id = var.mssql_database_id
authentication = { type = "systemAssignedIdentity" }8 · Target: Service Bus
target_resource_id = var.servicebus_namespace_id
authentication = { type = "systemAssignedIdentity" }9 · Client type for a .NET app
module "conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "storage"
app_service_id = var.web_app_id
target_resource_id = var.storage_account_id
authentication = { type = "systemAssignedIdentity" }
client_type = "dotnet" # generates .NET-flavored app settings
}10 · Private Link networking
vnet_solution = "privateLink" # keep target traffic off the public internet💡 Requires the target and app to support Private Link and the private connectivity to exist.
11 · Service Endpoint networking
vnet_solution = "serviceEndpoint"12 · Custom timeouts
module "conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "storage"
app_service_id = var.web_app_id
target_resource_id = var.storage_account_id
authentication = { type = "systemAssignedIdentity" }
timeouts = { create = "30m", delete = "30m" }
}13 · 🏗️ End-to-end composition
Create a web app and a storage account, connect them with the app's system identity, and grant that identity data-plane access to the target:
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-web-eastus"
location = "eastus"
}
module "plan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-service-plan.git?ref=v1.0.0"
name = "asp-contoso"
resource_group_name = module.rg.name
location = module.rg.location
os_type = "Linux"
sku_name = "P1v3"
}
module "web" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-linux-web-app.git?ref=v1.0.0"
name = "contoso-api"
resource_group_name = module.rg.name
location = module.rg.location
service_plan_id = module.plan.id
identity = { type = "SystemAssigned" } # enable the identity the connection uses
}
module "storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "contosodata"
resource_group_name = module.rg.name
location = module.rg.location
}
module "conn" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-app-service-connection.git?ref=v1.0.0"
name = "storage"
app_service_id = module.web.id
target_resource_id = module.storage.id
authentication = { type = "systemAssignedIdentity" }
client_type = "dotnet"
}
module "grant" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.storage.id
role_assignments = {
blob = {
scope = module.storage.id
role_definition_name = "Storage Blob Data Contributor"
principal_id = module.web.identity_principal_id
principal_type = "ServicePrincipal"
}
}
}💡 The connection wires the app to the target; the role assignment is what actually grants the identity access — Service Connector does not create it.
Required: name, app_service_id, target_resource_id, authentication (with type).
Optional: client_type, vnet_solution, secret_store.
Universal tail: timeouts (no tags — the resource does not support them).
Full schemas
name = string # required, force-new
app_service_id = string # required, force-new
target_resource_id = string # required, force-new
authentication = object({
type = string # required, force-new: systemAssignedIdentity | userAssignedIdentity
# | servicePrincipalSecret | servicePrincipalCertificate | secret
name = optional(string) # username for secret auth
secret = optional(string) # sensitive
certificate = optional(string) # sensitive
client_id = optional(string)
subscription_id = optional(string)
principal_id = optional(string)
})
client_type = optional(string) # none | dotnet | java | python | go | php | ruby | django | nodejs | springBoot
vnet_solution = optional(string) # serviceEndpoint | privateLink
secret_store = optional(object({ key_vault_id = string }))
timeouts = optional(object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) }))| Output | Description | Notes |
|---|---|---|
id |
Connection Resource ID | Emitted first |
name |
Connection name | |
target_resource_id |
Bound target resource ID | |
app_service_id |
The source App Service | 🔴 This module writes settings onto it |
target_resource_id |
The target backend | Force-new |
target_resource_type |
e.g. Microsoft.Storage/storageAccounts |
Parsed — the provider exposes no such attribute |
authentication_type |
The auth type in use | Unwrapped with nonsensitive() so plan review can see it |
uses_managed_identity |
Identity rather than a credential | Categorically stronger: nothing to rotate or leak |
supplies_a_credential |
A secret or certificate was supplied | Presence only — the value is never emitted |
credential_is_persisted_in_key_vault |
A secret store is set | Does not remove the secret from state |
credential_supplied_without_a_secret_store |
Credential in the app's settings | |
secret_store_key_vault_id |
The vault, or null |
The provider checks only non-emptiness; this module anchors the type |
client_type |
The stack settings are generated for | 🔴 Wrong value = app cannot find its configuration |
vnet_solution |
serviceEndpoint / privateLink / null |
Configures the expectation; does not build the path |
traffic_avoids_the_public_path |
A VNet solution is set | Not a posture verdict — the target's own firewall matters more |
settings_written_to_the_app_are_not_owned_here |
Always true |
🔴 A sibling module managing app_settings will fight with this |
connection_may_create_a_role_assignment_on_the_target |
Always true |
🔴 Needs User Access Administrator on the target; the assignment is unmanaged |
secrets_supplied_here_are_stored_in_state |
Always true |
🔴 sensitive redacts plan, not state |
this_module_emits_no_credential |
Always true |
Presence flags only, by design |
the_target_and_the_identity_are_not_verified |
Always true |
A connection can be created and be completely non-functional |
- Managed identity first. The default and recommended path is
systemAssignedIdentity/userAssignedIdentity, which needs no secret. Secret- and certificate-based types are supported but handle material out of band and are markedsensitive. - Wiring ≠ authorization. Service Connector configures app settings and networking, but the connection's identity still needs a data-plane role on the target — grant it with
terraform-azurerm-role-assignments. - Immutable core.
name,app_service_id,target_resource_id, andauthentication.typeare force-new; changing the target or auth mechanism replaces the connection. - No tags. The resource type does not support
tags, so the module omits the variable — the universal tail istimeoutsonly. secret_store. When a secret-based auth type is unavoidable,secret_storepersists the secret to Key Vault instead of the connection configuration.features {}dependence. The provider will not initialize without a caller-sidefeatures {}block — expected, and owned by the root module.
| Concern | Secure default | Opt-out (caller must type it) |
|---|---|---|
| Authentication | steer to managed identity (no secret) | choose servicePrincipalSecret / secret and supply material |
| Secret handling | secret / certificate wrapped sensitive; none emitted |
— |
| Secret persistence | secret_store in Key Vault when a secret is used |
omit secret_store |
| Networking | leave vnet_solution null unless private connectivity exists |
set serviceEndpoint / privateLink |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module by immutable tag (?ref=v1.0.0), never a branch. This is plan-only; a human applies from CI.
terraform validate+fmt -checkprove the type contract and the enum validations (authentication.type,client_type,vnet_solution) offline, before any Azure call.- Only
terraform planagainst a subscription exercises the identity configured on the App Service, the target's existence, and the data-plane role the identity needs.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/.../providers/Microsoft.Web/sites/contoso-api/providers/Microsoft.ServiceLinker/linkers/storage"
name = "storage"
target_resource_id = "/subscriptions/.../providers/Microsoft.Storage/storageAccounts/contosodata"
| Symptom | Cause | Fix |
|---|---|---|
authentication.type must be one of ... |
Unsupported auth type | Use one of the five documented values |
| App still gets 403 from the target | No data-plane role for the identity | Assign the identity the right role on the target (e.g. Storage Blob Data Contributor) |
| Identity auth fails at apply | Identity not enabled on the App Service | Enable system- or user-assigned identity on the app first |
| Connection replaced on every change | Changed a force-new field (target_resource_id, authentication.type, …) |
Expected — those fields replace the connection |
| Secret visible concern | Using a secret-based auth type | Prefer a managed identity; if not possible, set secret_store to keep it in Key Vault |
azurerm_app_service_connection- Azure Service Connector documentation
- Sibling modules:
terraform-azurerm-linux-web-app,terraform-azurerm-storage-account,terraform-azurerm-key-vault,terraform-azurerm-role-assignments - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."