DEV Community

Cover image for Back Up Every Azure Logic App to Git, Automatically
Vignesh Athiappan
Vignesh Athiappan

Posted on

Back Up Every Azure Logic App to Git, Automatically

If your Logic Apps are edited in the Azure portal (and let's be honest, most are), production probably lives only in the portal. That means:

  • No history: when a workflow breaks after an edit, you can't see what it looked like yesterday.
  • No undo for deletes: a deleted Logic App is gone, definition and all.
  • No source of truth: your repo, if you have one, drifts from what's actually running.

You don't need a big infrastructure-as-code migration to fix this. You need a daily, automatic snapshot of what's really running. Here's how I set one up in an afternoon.

The setup in one line

A scheduled pipeline reads Azure and commits to a locked backup branch. A second pipeline can restore any version from that branch. Both run from a separate, unprotected tooling branch.

What gets backed up

Everything is saved exactly as Azure returns it, one folder per resource, so a Git diff shows exactly what someone changed in the portal.

Resource Repo path Exported via
Consumption Logic Apps LogicApps/<name>/workflow.json ARM REST API (one GET per workflow)
API connections ApiConnections/<name>/connection.json ARM REST API
API Management APIM/<instance>/ Microsoft's open-source APIOps extractor

Two details matter:

  1. Fetch each resource individually. List calls can omit parts of the definition.
  2. Delete folders for resources that no longer exist. The latest commit mirrors Azure, while Git history keeps every older version.

Name clashes across resource groups get a __<resource-group> suffix so nothing is overwritten.

The daily pipeline

It runs on a Microsoft-hosted agent, so nobody's laptop needs to be on. Each run does four things:

  1. Clone the backup branch.
  2. Sign in with a Reader service connection and export everything.
  3. Stage only the backup folders.
  4. Commit and push, but only if something changed.
schedules:
  - cron: '30 1 * * *'        # daily, 01:30 UTC
    branches:
      include: [backup-tooling]
    always: true              # run even if the repo hasn't changed

jobs:
  - job: Backup
    steps:
      - checkout: self        # tooling branch: scripts only
      - script: git -c http.extraheader="AUTHORIZATION: bearer $SYSTEM_ACCESSTOKEN" clone --branch backup --single-branch "$(Build.Repository.Uri)" "$(backupDir)"
        env: { SYSTEM_ACCESSTOKEN: $(System.AccessToken) }
      - task: AzureCLI@2
        inputs:
          azureSubscription: my-reader-service-connection
          scriptType: bash
          scriptLocation: inlineScript
          inlineScript: python3 sync/export.py --subscription "$(subscription)" --out "$(backupDir)"
      - script: |
          cd "$(backupDir)"
          git add -A LogicApps ApiConnections APIM
          git diff --cached --quiet && { echo "No changes"; exit 0; }
          git commit -m "Daily Azure backup $(date -u +%F) [skip ci]"
          git push origin HEAD:backup
Enter fullscreen mode Exit fullscreen mode

Two easy-to-miss settings:

  • always: true makes the schedule fire even with no new commits.
  • [skip ci] stops the backup commit from triggering other pipelines.

Making the backup tamper-proof

A backup anyone can delete isn't a backup.

Don't use a branch lock. A lock blocks everyone except the person who set it, including the pipeline's build identity, and "Bypass policies" doesn't override it.

Use branch security instead (Repos → Branches → ⋯ → Branch security):

Who Contribute Force push
Contributors Deny Deny
Project Administrators Deny Deny
Project Build Service Allow Deny

In Azure DevOps, Deny beats Allow, and deleting a branch requires force-push. So no one, not even a project admin, can delete the branch or rewrite history, and only the pipeline can add commits.

Keep scripts on a separate branch. Once the backup branch is locked, you can't push script fixes to it either. An unprotected tooling branch solves that.

Restoring, safe by default

A second, manually triggered pipeline restores Logic Apps and connections from any backup version. The defaults are designed so a mis-click can't hurt production:

Parameter Default
Logic Apps ALL (or a comma-separated list)
Backup version latest (or any commit ID)
Target Original subscription/resource group (or a new one)
State after restore Disabled
Create missing connections Yes (existing ones never overwritten)
Dry run On

Under the hood it PUTs each workflow to the ARM REST API, rewrites resource IDs if the target differs, and creates the resource group if needed. At the end it prints a to-do list: connections to re-authorize, identities to re-grant, and trigger URLs that changed.

Tip: to check a restore you can't rehearse, wrap the same request bodies in an ARM template and run az deployment group validate. Azure checks everything and deploys nothing.

What a backup can't give you

This protects your definitions, not your whole environment:

Gap What to do after restore
Connection passwords / OAuth sign-ins Re-authorize connections (Office 365, Teams, SharePoint…)
HTTP trigger URLs Re-created apps get new URLs, so update callers (overwriting in place keeps the URL)
Managed identity access Re-grant roles on Key Vault, Service Bus, etc.
Run history Not exportable. Accept the loss
Secrets in definitions Exported as-is. Keep the repo private or move secrets to Key Vault

Rule of thumb: if the resource group and connections still exist, a restore takes minutes. If the whole subscription is gone, expect hours, mostly re-signing connections and updating callers.

Lessons learned

  • Count with the pipeline's identity, not yours. My personal login saw a quarter of the Logic Apps; the service connection saw all of them.
  • Don't trust az resource list counts. Paged results can contain duplicates, so count unique resource IDs.
  • Commit only on change. Your history becomes a log of real portal edits, not daily noise.
  • Pin third-party tools. Pin the APIM extractor version so a new release can't change output overnight.
  • Grant the build identity Contribute. Otherwise the first push fails with TF401027.

Wrap-up

One scheduled pipeline, one protected branch, and one restore pipeline give you daily, tamper-proof history of every Logic App, API connection, and APIM instance. Each morning's commit shows exactly what changed in production the day before, and any version can be put back.

Top comments (0)