Logic App API connections (Microsoft.Web/connections) with managed identity and oAuth done right, via azapi.
API connections are the credential half of every Logic App connector, and the reason playbook
deployments fight Terraform: the native azurerm_api_connection resource cannot express managed
identity auth or the oAuth consent flow, and naive management makes every portal consent a diff.
This module drives the resource through azapi instead:
- Managed identity auth.
use_managed_identity = truesetsparameterValueType = "Alternative": no credentials stored, the consuming Logic App presents its identity. This is the shape Sentinel playbooks should use. - oAuth without stored secrets. A named
parameter_value_setcreates the connection unauthenticated; a human consents once in the portal, and because parameter values are ignored after create, the consent sticks instead of being reverted on the next apply. - Key-based connectors still work. Secret values ride in a separate sensitive
parameter_valuesvariable keyed by connection name (the definitions stay printable), next to inline non-secret and custom parameter values; a check flags secret entries that the chosen auth shape would ignore. - Managed APIs by short name.
managed_api_name = "office365"resolves against the subscription and region; a fullmanaged_api_idbinds custom connectors.
Requires Terraform >= 1.9, azapi >= 2.0, and azurerm >= 4.0 (for its client-side
parse_resource_id function only). Pairs with the sentinel-automation-rule module (playbooks) and
any Logic App deployment.
module "api_connection" {
source = "libre-devops/api-connection/azapi"
version = "~> 4.0"
resource_group_id = module.rg.ids["rg-ldo-uks-prd-001"]
location = "uksouth"
tags = module.tags.tags
api_connections = {
"azuresentinel-mi" = {
managed_api_name = "azuresentinel"
use_managed_identity = true
}
"office365-oauth" = {
managed_api_name = "office365"
parameter_value_set = { name = "oauthMI" }
}
"azureblob-key" = {
managed_api_name = "azureblob"
non_secret_parameter_values = { accountName = "stldouksprd001" }
}
}
parameter_values = {
"azureblob-key" = {
accountName = "stldouksprd001"
accessKey = module.storage.primary_access_keys["stldouksprd001"]
}
}
}examples/minimal- one managed identity connection.examples/complete- all three auth shapes plus a full-id binding, with a real storage key wired through the sensitive variable.
Local work needs PowerShell 7+ and just, because the recipes
wrap the LibreDevOpsHelpers
PowerShell module (the same engine the libre-devops/terraform-azure action runs in CI). Install
just with brew install just, or uv tool add rust-just then uv run just <recipe>.
Run just to list recipes: just update-ldo-pwsh (install or force-update LibreDevOpsHelpers from
PSGallery), just validate, just scan (Trivy only), just pwsh-analyze (PSScriptAnalyzer only),
just plan, just apply, just destroy, just e2e, just test, and just docs (the
plan/apply/destroy recipes mirror the action, including the storage firewall dance; just e2e
applies an example then always destroys it, defaulting to minimal, so nothing is left running).
Releasing is also just:
just increment-release [patch|minor|major] bumps, tags, and publishes a GitHub release, and the
Terraform Registry picks up the tag.
This module is scanned with Trivy; HIGH and CRITICAL
findings fail the build. Any waiver is a deliberate, reviewed decision, never a way to quiet a
finding that should be fixed. Waivers live in a .trivyignore.yaml (the machine-applied source of
truth, passed to Trivy with --ignorefile) and are mirrored in a table here so the reason is
auditable.
There are currently no exceptions: the module and its examples scan clean.
The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.
| Name | Version |
|---|---|
| terraform | >= 1.9.0, < 2.0.0 |
| azapi | >= 2.0.0, < 3.0.0 |
| azurerm | >= 4.0.0, < 5.0.0 |
| Name | Version |
|---|---|
| azapi | >= 2.0.0, < 3.0.0 |
No modules.
| Name | Type |
|---|---|
| azapi_resource.this | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| api_connections | API connections (Microsoft.Web/connections, the credential half of a Logic App connector) keyed by name; the key doubles as the display name unless display_name is set. The API to bind is managed_api_name (the short connector name: azuresentinel, office365,keyvault, azureblob, ...), resolved against the subscription and location, or a full managed_api_id for custom connectors.Authentication, one of three shapes: - use_managed_identity = true: the connection authenticates with a managed identity(parameterValueType Alternative), no credentials stored. - parameter_value_set: named auth scheme with partial values (an oAuth connection is createdunauthenticated and consented once in the portal; the consent survives Terraform runs because parameter values are ignored after create). - secret parameter_values (via the separate sensitive variable) and/or inlinenon_secret_parameter_values / custom_parameter_values for key- and token-style connectors.Parameter values, timestamps, and Azure-managed API metadata are ignored after create, so portal consent flows and service-side updates never fight Terraform. |
map(object({ |
{} |
no |
| api_version | The Microsoft.Web/connections API version azapi drives. | string |
"2016-06-01" |
no |
| location | The Azure region the connections (and the managed APIs they bind to) live in. | string |
n/a | yes |
| parameter_values | Secret parameter values (connection strings, keys, tokens) keyed by the same connection name used in api_connections. Kept out of the main map so the connection definitions stay non-sensitive; ignored after create, so rotation happens by replacing the connection or updating out-of-band. | map(map(string)) |
{} |
no |
| resource_group_id | The id of the resource group the API connections land in. Parsed for the subscription (to resolve managed API names) and the resource group name. | string |
n/a | yes |
| schema_validation_enabled | Whether azapi validates the request body against its embedded ARM schema before sending it. Off by default: the embedded 2016-06-01 connections schema does not model parameterValueType (managed identity) or parameterValueSet (oAuth) even though ARM accepts both. | bool |
false |
no |
| tags | Tags applied to every connection (merged with any per-connection tags). | map(string) |
{} |
no |
| Name | Description |
|---|---|
| api_connection_ids | Map of connection name to its id. |
| api_connection_ids_zipmap | Map of connection name to { name, id }, for easy composition with other modules (Logic App workflow parameters, for example). |
| api_connections | Map of connection name to { id, name, display_name, managed_api_id, and the exported api metadata, statuses, and parameter value type }. |