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.
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/workflowstype. A workflow is a file,<name>/workflow.json, inside a package the Functions host loads alongsideconnections.jsonandhost.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
azurermzip_deploy_filepath 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.
| 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 |
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.
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"]
})
}
}
}- Build it in the designer.
- Download app content from the app's Overview blade, or copy one workflow's code view.
- Paste each workflow into
templates/<name>.json.tftpl. - Ctrl+F the values Terraform owns into
${tokens}. - 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.
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.
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.
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.
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. |
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:
-
A real defect in this repository's example, now fixed. The example's package storage account had
default_action = "Deny"with theAzureServicesbypass, added to satisfy a Trivy finding. The apply failed with403 This request is not authorizedwriting 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 inexamples/minimal/main.tf. -
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.