Skip to content

Repository files navigation

Terraform AzAPI Logic App Standard Workflows

Logic App Standard workflow content as code: workflows rendered from portal-shaped templates by templatefile, packaged with connections.json and host.json, and deployed through the ARM ZipDeploy extension, so a content change plans before it applies and needs no publishing profile.


Overview

This is the Standard counterpart to terraform-azapi-logic-app-workflow, and it is deliberately not a port of it, because the platform is not the same underneath.

On Standard, a workflow is not an ARM resource. There is no Microsoft.Web/sites/workflows type. A workflow is a file, <name>/workflow.json, inside a package the Functions host loads alongside connections.json and host.json.

So the Consumption module's whole shape (one PUT per workflow, per-workflow lifecycle, deploy_tier ordering) has no equivalent here, and imitating it would be a lie about the platform.

What is an ARM resource is the deployment: Microsoft.Web/sites/extensions named ZipDeploy, carrying a packageUri the platform fetches. That is why this module is azapi rather than a shell script, and it buys two things that matter:

  • No publishing profile. ZipDeploy through ARM is a control-plane call, so basic authentication can stay disabled on the site. The azurerm zip_deploy_file path needs the basic-auth publishing profile, which is why the Libre DevOps function app modules disable it and tell you to push from outside Terraform.
  • It plans. The package hash is in the plan, so a content change is visible before apply, and the hash is the promotion and rollback record.

What carries over from Consumption, and what does not

Consumption module This module
Export you start from portal code view portal Download app content zip
Artefact one definition workflow.json per workflow, plus connections.json and host.json
Deployed by one PUT to Microsoft.Logic/workflows one package, one ZipDeploy extension
templatefile token contract identical identical
Ordering deploy_tier not needed: one package, loaded together
Lifecycle per workflow create/update/destroy whole-app content replace
Parameter values ARM body parameters app settings and @appsetting(), owned by the host
Secrets sensitive_body app settings or Key Vault references, owned by the host
Connections V1, access policies rejected V2, access policies required

It owns content, never the host

The plan, storage account and site come from libre-devops/logic-app-standard/azurerm or your own configuration. Two reasons, both practical: the host module is mature and duplicating it to make one call look tidier is a bad trade, and app settings have exactly one owner. A content module that also wrote WEBSITE_RUN_FROM_PACKAGE would fight the host module over the same attribute on every apply. ZipDeploy touches no app settings at all, which is the other reason to prefer it.

Usage

module "logic_app_workflows" {
  source  = "libre-devops/logic-app-standard-workflows/azapi"
  version = "~> 1.0"

  logic_app_id = module.logic_app_standard.logic_app_ids["logic-ldo-uks-prd-01"]

  package_storage = {
    storage_account_id = azurerm_storage_account.packages.id
    container_name     = azurerm_storage_container.packages.name
  }

  managed_api_connections = {
    "azuresentinel" = {
      connection_id          = azapi_resource.sentinel_connection.id
      managed_api_id         = data.azurerm_managed_api.sentinel.id
      connection_runtime_url = azapi_resource.sentinel_connection.output.properties.connectionRuntimeUrl
      managed_identity_auth  = true
    }
  }

  workflows = {
    "incident-ack" = {
      definition = templatefile("${path.module}/templates/incident-ack.json.tftpl", {
        workspace_id = module.law.workspace_ids["log-ldo-uks-prd-001"]
      })
    }
  }
}

Authoring: the same round trip as Consumption

  1. Build it in the designer.
  2. Download app content from the app's Overview blade, or copy one workflow's code view.
  3. Paste each workflow into templates/<name>.json.tftpl.
  4. Ctrl+F the values Terraform owns into ${tokens}.
  5. Plan.

Both shapes paste in unchanged: a Standard workflow.json ({"definition": {...}, "kind": ...}) and a bare definition. A definition has no top-level definition key of its own, so the unwrap is unambiguous, and the definition itself is never reshaped, which is what keeps a committed template diffable against a fresh export.

Point your editor at the annotated WDL schema while you write: it validates the definition block of a Standard workflow.json too, and unlike the published schema it does not redline correct code.

The connections.json trap

Locally, Visual Studio Code writes managed API connections with a Raw scheme and an appsetting key. Azure expects ManagedServiceIdentity. Releasing the local shape unchanged is the classic Standard deployment failure, so managed_identity_auth (default true) writes the Azure format for you.

Standard uses V2 connections, which require an access policy granting the app's identity access. Consumption V1 connections reject access policies outright. This module does not create those policies: they belong with the connection, not with the content.

Guard rails

Anything the platform rejects fails the plan, as a variable validation: a definition that is not JSON, a workflow with no trigger, an empty workflow map, a bad logic_app_id, an unusable workflow name. Anything that deploys cleanly and bites later is a check, which warns: a connection no workflow references, key authentication where managed identity would do, a Stateless workflow, a SAS window longer than a day, a package over 100 MB.

Developing

terraform init -backend=false && terraform test    # offline: mocked providers, no Azure, no cost
terraform fmt -recursive -check
tflint --recursive
trivy config --skip-dirs '**/.terraform/**' .

terraform test runs seven cases with mocked providers: the package layout, bare-definition wrapping, the Azure authentication format in connections.json, and four rejection cases proving the plan-time guards actually fire.

Security scan exceptions

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.

Trivy ID Resource Finding Justification
AVD-AZU-0012 azurerm_storage_account in libre-devops/logic-app-standard/azurerm Storage account network rules default action should be Deny Raised against the dependency host module this module composes with and does not own. Trivy follows registry module sources, so a finding inside the host module surfaces here even though nothing in this repository declares that resource. This module's own example sets default_action = "Deny" with the AzureServices bypass, so it is fixed wherever this repository is responsible. The host module carries its own documented waiver for the same id, and its reasoning holds: a Logic App Standard reaches its runtime storage from platform ranges, so deny-by-default would break the app and the working lockdown is VNet integration plus private endpoints, which is caller topology. Scoped to downloaded module paths only, so it can never mask a finding in this repository's own code.

Status

Offline gates green. Live path PARTLY exercised, ZipDeploy itself still unproven.

terraform test passes seven cases with mocked providers, and validate, fmt, tflint and trivy are clean. A live self-test was dispatched on 24 August 2026 and reached a real apply. It did not complete, for two reasons, both recorded here rather than quietly fixed:

  1. A real defect in this repository's example, now fixed. The example's package storage account had default_action = "Deny" with the AzureServices bypass, added to satisfy a Trivy finding. The apply failed with 403 This request is not authorized writing the package blob. The bypass covers the platform fetching the package; it does not cover whatever runs Terraform writing it. Deny-by-default on a deployment-package account breaks the deploy rather than securing it. See the comment in examples/minimal/main.tf.

  2. Subscription quota, not a code problem. Creating the Workflow Standard plan failed with Current Limit (Total VMs): 0. The test subscription has no compute quota, so no Logic App Standard host can be created in it at all. This is why the Consumption sibling can self-test live and this one cannot: Consumption needs no plan.

So what is proven today: the module plans, the package builds, the rendering is correct, and the configuration is accepted up to the point the host could not be created. What is not proven is the ZipDeploy extension actually landing content on a running Logic App Standard. That needs a subscription with compute quota, and until someone runs it there, treat that step as asserted from Microsoft's documentation rather than observed.

The teardown ran and succeeded, so the failed run left nothing behind.

About

Logic App Standard workflow content as code: workflows rendered by templatefile, packaged with connections.json and host.json, deployed through the ARM ZipDeploy extension.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages