Azure API Management is easy to change in the portal, and that's the problem. If your APIs and policies were built by hand, the only copy lives in Azure, and there's no "backup" button:
- A deleted APIM instance is soft-deleted for 48 hours, then purged with every API, policy and secret inside it.
- Portal edits leave no history. When a policy breaks, nobody can say what it looked like yesterday.
We wanted a daily copy we could trust, a history of every change, and a restore we had actually tested.
The idea: Azure is the truth, Git is the time machine
We didn't force a move to infrastructure-as-code. Azure stays the source of truth, and Git records what APIM looked like every morning.
- Export exactly what Azure returns. No templating, no clean-up.
- Mirror, don't accumulate. Deleted in Azure means deleted from the latest backup. Git history keeps every older version.
- Commit only on change. Every commit is a real change worth looking at.
- Run in CI, not on a laptop.
Bonus: the backup branch doubles as an audit log for policies that never had version control.
How the backup works
One scheduled job:
- Check out the backup branch into its own folder. Pipeline code lives on a separate tooling branch; backups live on a protected branch only the build identity can push to.
- Extract APIM with Microsoft's open-source APIOps extractor, pinned to a fixed version. It writes OpenAPI specs, policies, products, backends, named values, policy fragments and loggers.
-
Add
service.jsonwith the instance's own settings (tier, region, identity, network), because the extractor skips them. -
Mirror with
rsync --deleteso deletions show up too. - Commit and push only if
git diffis non-empty.
Repo layout:
APIM/<instance>/service.json
APIM/<instance>/policy.xml
APIM/<instance>/apis/<api>/specification.yaml, policy.xml, operations/…
APIM/<instance>/backends, named values, products, policy fragments, …
Exporting needs only Reader access. A morning check confirms the run started, every step passed and the push landed, and alerts if not.
What about secrets?
Azure doesn't return secret values through its APIs, so they never reach Git. That's safe, but it means a backup alone can't bring them back.
| Item | In the backup? | Notes |
|---|---|---|
| Secret named values | Name only | Value is blank |
| Plain-text named values | Yes, readable | Plain text in Azure = plain text in Git |
| Subscription keys | No | Metadata only |
| Keys typed directly into policies | Yes, readable | Scan before the first commit |
Two takeaways:
- Hunt for credentials stored as plain-text named values or hardcoded in policy XML. Mark them secret in Azure, or accept the risk knowingly.
- Treat the backup repo as sensitive. Restrict read access to a small group plus the build identity.
Restoring APIM
A backup you've never restored is a hope, not a plan.
The restore pipeline is manual and defaults to dry run: the first run only reports what it would do. It checks the target first and picks a path:
| Target instance is… | What happens | Result |
|---|---|---|
| Soft-deleted (< 48h) | Recovered in place | Everything back: secrets, keys, identity, same URL |
| Missing | Re-created from service.json, then published with the APIOps publisher |
APIs, policies, products, backends back; secrets partly |
| Still there | Backup published into it | Same-named items overwritten, others untouched |
The trick: automatic secret refill
If you front Logic Apps with APIM, most of your secret named values are the sig keys APIM stores when you import a Logic App as an API. The restore script:
- Reads each operation's policy to find which Logic App trigger it calls.
- Asks that Logic App for its current callback URL, which gives back the
sigkey. - Repoints the APIM backend if the Logic App was re-created with a new URL.
Application Insights logger keys are read from the App Insights resource the same way. In our dry run, this refilled about 80% of secrets automatically. Most of the rest belonged to Logic Apps that no longer existed.
If APIM fronts Logic Apps, restore the Logic Apps first, so APIM can read their keys and URLs.
What stays manual after a full rebuild
- Secrets with no source to read from (client secrets, partner keys)
- New subscription keys, which every caller has to receive
- Access for the new managed identity (Key Vault, storage, AI services)
During an incident
Pause the daily backup. It mirrors Azure, so a run after a deletion makes "latest" empty. Git history still has everything, but don't make recovery harder for yourself.
Lessons learned
- Use the vendor's tool. Saving APIM is easy; restoring it in the right order (named values → backends → APIs → operations → policies → products) isn't. APIOps already solves that.
- Pin versions. A newer extractor that formats files differently produces a giant diff that hides real changes.
- Know your recovery window. 48 hours of soft-delete is your only safety net without a backup.
- Document the gaps honestly. Ours: single subscription only, non-production APIM tier.
- Think twice about delete locks. A delete lock on APIM also blocks deleting anything inside it, like an API or a named value, which gets in the way of everyday work.
Checklist to build your own
- [ ] Dedicated repo with a tooling branch and a protected backup branch
- [ ] Service connection with Reader for export (Contributor only if it also restores)
- [ ] Daily APIOps extraction (pinned) plus a
service.jsonexport - [ ] Mirror with deletions; commit only on change
- [ ] Scan the first export for plain-text secrets; restrict repo access
- [ ] Dry-run-first restore pipeline, run at least once
- [ ] Daily check that alerts when the backup didn't run or failed
- [ ] One-page runbook: restore order, what stays manual, who to call
- [ ] Rehearse a full restore into a throwaway resource group
Top comments (0)