Azure
Azure
Scope: CLI and API only. This guide covers the az CLI and the ARM templates for onboarding Azure subscriptions without the console wizard. Firefly also ships a Terraform module for Azure onboarding, documented separately.
Check which Firefly site your tenant is on before you deploy. Both templates have a fireflySite parameter with allowed values firefly.ai and eu.firefly.ai, defaulting to firefly.ai. If you deploy with the default for an EU-hosted tenant, the embedded script registers against the wrong region and the integration won't appear in your console.
Set location to match your data residency requirements. It defaults to westus2 in both templates and controls where the storage account holding your Azure activity logs is created. Leaving the default for an EU-hosted tenant puts activity logs in a US region — set it to an appropriate European region alongside fireflySite: eu.firefly.ai.
When to use this
Use this guide to bulk onboard Azure subscriptions, drive onboarding from a CI/CD pipeline, or give a security team the whole integration as a reviewable template before it runs. For a single subscription, the console wizard (Settings > Integrations > Add New > Azure) is faster.
Azure's flow differs from AWS and Google Cloud: the templates contain a deployment script that authenticates to Firefly and registers subscriptions for you. There's no separate manual registration call at the end — your Firefly access key and secret key are template parameters.
Choosing a flow
There are two templates in the repository, and they are not variants of the same deployment — they differ in scope, architecture, and the permissions Firefly ends up with.
Flow 1: Subscription-scoped
Flow 2: Management-group-scoped
Template
azurefireflydeploy.json
azurefireflydeploy-managementgroups.json
Deployment command
az deployment sub create
az deployment mg create
Which subscriptions
Explicit list in targetSubscriptions
Every subscription under the management group, discovered recursively at deploy time
Monitoring resources
One resource group per subscription (firefly-monitoring-<subId>)
One shared resource group in a hub subscription (firefly-monitoring-mg-<mgId>)
Role assignments
Written into each subscription individually
Written once at the management group scope, inherited by every subscription below
Firefly custom role
Created and assigned in every target subscription
Created and assigned in the hub subscription only (see the permission gap below)
Best for
A known, stable set of subscriptions; tenants with no management group hierarchy
Large estates, whole hierarchies, or customers who add subscriptions regularly
Rule of thumb: one subscription → the console wizard. A handful of known subscriptions → Flow 1. A hierarchy, or more subscriptions than you want to list by hand → Flow 2.
The two flows aren't mutually exclusive. A common pattern is Flow 2 across the hierarchy for broad inventory, plus Flow 1 targeted at the few subscriptions where full IaC and secret-adjacent discovery is required — see the permission gap under Flow 2.
Prerequisites
Common to both flows:
Admin access to the Firefly console (to create the API key pair)
Azure CLI, authenticated
Application Administrator or Global Administrator in Entra ID, to create the service principal
The Entra ID primary domain of your tenant —
directoryDomainis a required parameter in both templates with no defaultgit, andjqif you script the parameter fileThese providers registered in the subscription hosting the deployment:
Microsoft.Storage,Microsoft.EventGrid, andMicrosoft.ContainerInstance(the deployment scripts run as container instances)
Flow 1 additionally needs:
Owner or Contributor on every subscription in
targetSubscriptionsUser Access Administrator or Owner on those subscriptions, to create role assignments
Flow 2 additionally needs:
Owner or User Access Administrator at the management group scope — role assignments are written at the management group, not per subscription
Contributor on the hub subscription named in
subscriptionIdForDeploymentAll subscriptions in the same Entra ID tenant. If you have more than one tenant, you need a separate service principal, hub subscription, and deployment for each
Elevated access, if you're targeting the Tenant Root Group — see "Which management group to target" below
Phase A — Firefly key pair
Create the key pair under Settings > Users > Create Key Pair. Copy both values; they are shown once. These go straight into the deployment parameters, so treat the parameter file as a secret from the moment you create it.
Phase B — Clone and authenticate
azurefireflydeploy.json
Flow 1, subscription-scoped template
azurefireflydeploy-managementgroups.json
Flow 2, management-group-scoped template
CreateUIDefinition.json
Azure Portal guided-deployment UI, Flow 1
CreateUIDefinition-managementgroups.json
Azure Portal guided-deployment UI, Flow 2
README.md
Repository documentation
Phase C — Service principal
Create it once. What differs between flows is the scope you grant it.
Flow 1 — scope to the subscription:
Flow 2 — scope to the management group:
The management-group-scoped grant covers every subscription in the hierarchy through inheritance, so there's no per-subscription role assignment loop in Flow 2. The template writes its own management-group-scope assignments during deployment, so this initial grant is belt-and-braces — you can omit --role and --scopes entirely and let the template do all of it.
The output contains appId, password, and tenant. The password cannot be retrieved again — capture it now. Then fetch the object ID, a different value from appId, which the templates need for role assignments:
appId
servicePrincipalClientId
password
servicePrincipalClientSecret
id (from az ad sp show)
servicePrincipalObjectId
The secret has an expiry date, and the integration fails silently when it lapses. az ad sp create-for-rbac issues a client secret with a finite lifetime — commonly one year, depending on tenant policy. Nothing warns you beforehand; asset collection simply stops and Inventory quietly goes stale. Record the expiry at onboarding and set a reminder. See Credential rotation below.
Flow 1: Subscription-scoped deployment
Use for a known list of subscriptions. The template loops over targetSubscriptions and provisions a full, independent monitoring stack into each one.
1. Set subscription context
2. Build the parameters file
location
string
westus2
Region for the Firefly monitoring resources. Set it for data residency
fireflySite
string
firefly.ai
Set to eu.firefly.ai for EU tenants. Drives both the API base and the event webhook host
directoryDomain
string
required, no default
Entra ID domain. The deployment fails validation without it
servicePrincipalObjectId
string
required
Object ID, not app ID
servicePrincipalClientId
string
required
appId
servicePrincipalClientSecret
securestring
required
password
fireflyAccessKey
securestring
required
Used by the embedded script to authenticate
fireflySecretKey
securestring
required
As above
targetSubscriptions
array
current subscription
Every subscription to onboard
isMultiSubscription
bool
true
Template defaults to true; set false for a single subscription
eventDrivenEnabled
bool
true
Event Grid real-time monitoring
isProd
bool
false
Marks the integration as production
enforceStorageNetworkRules
bool
false
Applies network rules to the log storage account, restricted to Firefly egress IPs
fireflyWebhookUrl
string
empty
Overrides the default https://azure-events.{fireflySite}. Leave empty unless instructed otherwise
fireflyEips
array
Firefly egress IPs
Fallback only. A deployment script fetches the live list from https://api.{fireflySite}/v2/infrastructure/public-nat-ips and only falls back to this array if that call fails
tags
array
[]
Applied to created resources
The parameter file holds the service principal secret and both Firefly keys in plaintext. Add it to .gitignore before you populate it, and delete it from your workstation once the deployment succeeds.
3. Validate and preview
what-if is worth running in front of your security team — it shows exactly what the template will create before anything is committed.
4. Deploy
Expect 5–15 minutes. Per subscription in the array, the template creates a resource group (firefly-monitoring-<subscriptionId>), a storage account for activity logs, an Event Grid system topic and subscription, custom role definitions, role assignments, and diagnostic settings — then runs the deployment script that registers everything with Firefly.
Locked-down subscriptions: the registration step runs as an ARM deploymentScripts resource, which provisions its own storage account and container instance behind the scenes. Azure Policy that denies public storage endpoints or mandates private endpoints will block it, and the failure surfaces as "deployment succeeded, script failed" — which looks like a Firefly problem but is a policy problem. Check for such policies before deploying into a regulated subscription.
5. Adding more subscriptions
Grant the service principal Reader on each additional subscription, then list them all in targetSubscriptions:
Set isMultiSubscription to true and redeploy. All subscriptions must sit in the same Entra ID tenant.
If this loop is getting long, you want Flow 2.
Flow 2: Management-group-scoped deployment
Use for bulk onboarding across a hierarchy. You name one management group; the template discovers every subscription beneath it recursively, including subscriptions in nested child management groups, and registers each one as its own Firefly integration.
How this flow is architecturally different
This is not simply "Flow 1 with a wider net" — read this before deploying.
Role assignments are written once, at the management group scope, and inherit downward. Six built-in roles: Reader, Billing Reader, App Configuration Data Reader, Security Reader, Monitoring Reader, and Management Group Reader. The last two don't appear in Flow 1 at all.
Monitoring infrastructure is centralized, not per-subscription. One resource group (
firefly-monitoring-mg-<managementGroupId>), one storage account, and one Event Grid system topic are created in the hub subscription you nominate viasubscriptionIdForDeployment. Every subscription in the hierarchy gets a diagnostic setting pointing at that single shared storage account. Flow 1, by contrast, builds a complete stack in each subscription.A diagnostic setting is also written at the management group scope itself, capturing management-group-level activity, in addition to the per-subscription ones.
Each subscription still becomes its own Firefly integration. The management group is the deployment scope, not the unit of integration. A 200-subscription hierarchy produces 200 integrations in the console, named after each subscription's display name.
Permission gap versus Flow 1 (read this)
The Firefly custom role and the conditional Storage Blob Data Reader assignment are created with assignableScopes limited to the hub subscription, and are only assigned there — they are not propagated across the management group.
In practice, every subscription other than the hub gets read-level inventory and billing visibility from Firefly, but not the elevated permissions the custom role grants (storage account keys, database connection strings, AKS cluster credentials, web app configuration, Redis keys, search service keys, and Log Analytics workspace keys), and not the conditional blob access used to read Terraform state objects.
Consequence: Terraform state discovery and IaC mapping won't work in non-hub subscriptions under a pure Flow 2 deployment. If you need that, either run Flow 1 additionally against the subscriptions holding your state backends, or reach out to Firefly about it. Don't assume the management-group deployment covers it — set this expectation during a POC rather than after.
Which management group to target
Don't reflexively pick the Tenant Root Group. Two things make a lower, more specific management group the better choice in most estates.
isProd and tags apply to the whole deployment — they're single values, written identically to every subscription the run discovers. A real hierarchy contains production and non-production subscriptions side by side, so one root-level run mislabels a large part of the estate in Firefly and makes your environment filters useless.
The pattern that works is one run per branch: a deployment against the production management group with isProd: true and production tags, then a second against the non-production management group with its own values. Both runs can share the same service principal and hub subscription — only the parameter file changes. Split further if tagging conventions differ by business unit.
The Tenant Root Group needs elevated access. Deploying at root requires a Global Administrator to enable "Access management for Azure resources" in Entra ID, which grants them User Access Administrator at root, and then to assign the deploying identity there. Without it, the role assignment step fails with an authorization error that never mentions the root group:
Have the Global Administrator reverse the elevation once the deployment completes. If a lower management group covers the subscriptions you care about, targeting it avoids this step entirely.
1. Inspect the hierarchy first
Always do this before deploying — it tells you the management group ID to use and exactly how many subscriptions you're about to onboard.
Note the ID (name field), not the display name — managementGroupId expects the ID.
To count the subscriptions you're about to onboard, use the descendants API. A JMESPath query against children returns only the direct children of the management group, so on any hierarchy deeper than one level it undercounts — you'd later conclude the deployment failed when it didn't:
Record that number — it's what you check the deployment against afterward. If the management group contains subscriptions you didn't intend to onboard, move them or pick a lower management group in the tree. There's no exclusion parameter.
2. Choose the hub subscription
subscriptionIdForDeployment is required and has no default. It nominates the subscription that will host the shared monitoring resources and run the deployment scripts. Choose one that is:
long-lived and not scheduled for decommissioning
not subject to Azure Policy that blocks public storage endpoints or container instances
the subscription holding your Terraform state backends, if you have a central one — that maximizes what the custom role can reach given the permission gap above
3. Build the parameters file
Note the different $schema — a subscription-scoped parameters file won't validate here.
Parameters that differ from Flow 1:
managementGroupId
string
current management group
The management group ID, not display name. Defaults to the group you deploy into, but set it explicitly
subscriptionIdForDeployment
string
required, no default
Hub subscription hosting the shared resource group, storage account, Event Grid topic, and the deployment scripts
targetSubscriptions
n/a
n/a
Does not exist in this template. Subscriptions are discovered, not listed
isMultiSubscription
n/a
n/a
Does not exist in this template. Multi-subscription is implicit
All other parameters (fireflySite, directoryDomain, the service principal trio, the Firefly key pair, eventDrivenEnabled, isProd, enforceStorageNetworkRules, fireflyWebhookUrl, fireflyEips, tags) behave exactly as in the Flow 1 table above.
4. Validate and preview
--location is mandatory for management-group-scoped deployments even though the management group itself isn't regional — it sets where deployment metadata is stored.
what-if will show the management-group-scope role assignments and the hub-subscription resources, but not the per-subscription diagnostic settings or the Firefly integrations — those are created by deployment scripts at runtime and are invisible to what-if. If a security team asks why the preview looks smaller than expected, that's why.
5. Deploy
Expect longer than Flow 1. Both scripts process subscriptions serially, with a deliberate pause between each, so allow roughly 15–30 minutes for a large hierarchy.
There's a ceiling. The deployment scripts have a 30-minute timeout and no resume, so a hierarchy large enough to exceed it fails partway with no way to continue from where it stopped. Somewhere in the low hundreds of subscriptions is where this starts to matter. At that scale, split the work by child management group and run one deployment per branch — which is also what you want for isProd accuracy.
6. What the deployment scripts actually do
Three scripts run, all as AzurePowerShell deployment scripts in the hub subscription.
IP fetch. Calls
https://api.{fireflySite}/v2/infrastructure/public-nat-ipsfor the live Firefly egress IP list, falling back to thefireflyEipsarray if unreachable.Diagnostics. Authenticates as the service principal, walks the management group hierarchy recursively, and writes a diagnostic setting named
firefly-mg-diagnostics-<subId>into each subscription, covering Administrative, Security, ServiceHealth, Alert, Recommendation, Policy, Autoscale, and ResourceHealth logs — all targeting the shared hub storage account.Integration. Authenticates to Firefly, waits 30 seconds for role assignments to propagate, walks the hierarchy again, and registers each subscription:
onConflictUpdate=true means re-running the deployment updates existing integrations rather than failing, so redeploys are safe.
Behavior worth knowing:
Disabled subscriptions are skipped silently. If the count in Firefly is lower than the count in the management group, check subscription state first.
Integration names are the subscription display names, sanitized. Anything outside
A-Z a-z 0-9 space _ : . @ / + , -is replaced with a hyphen, runs of hyphens are collapsed, and leading non-alphanumerics are stripped. A subscription whose name is entirely special characters falls back to its subscription ID. Expect cosmetic name differences between Azure and Firefly for decorated naming conventions.A per-subscription failure does not fail the deployment. The script logs a warning, adds the subscription to a failed list, and continues — it only errors out if every subscription failed. Always read the script output (see Verify).
The Firefly API can return HTTP 200 with a validation error in the body. The script detects this and counts it as a failure, but the deployment still reports success overall.
7. The auto-discovery caveat: correct expectation setting
The repository README describes "automatic discovery of new subscriptions." That's partially true, and it's the most common source of a wrong customer expectation. Be precise:
RBAC role assignments
Inherited automatically. Management-group-scope assignments apply to the new subscription immediately, no action needed
Diagnostic settings
Not created. The diagnostics script only ran at deploy time
Firefly integration / registration
Not created. The new subscription won't appear in the Firefly console
This is not the equivalent of AWS StackSet auto-deployment. To pick up subscriptions added after the initial deployment, re-run the same az deployment mg create command. It's idempotent: onConflictUpdate=true updates existing integrations rather than duplicating them, and existing diagnostic settings are updated in place.
Set up either a scheduled redeploy (a pipeline running the same command monthly, or on subscription-vending events) or a documented runbook step in your subscription provisioning process. Raise this during a POC rather than discovering it three months later with a stale inventory.
8. Management-group-specific failure modes
Missing Management Group Reader is the dangerous one. If the service principal can't enumerate the hierarchy, the script doesn't fail — it catches the error, logs a warning, and falls back to onboarding only the hub subscription. The deployment reports success. One integration appears instead of two hundred, and unless you read the script log the cause is invisible. If the subscription count is wrong, check this first.
Azure Policy blocking deployment scripts. The registration and diagnostics steps run as ARM deploymentScripts, which provision their own storage account and container instance in the hub subscription. Policy that denies public storage endpoints, mandates private endpoints, or restricts container instances will block them, and the failure surfaces as "deployment succeeded, script failed" — which reads as a Firefly problem when it's a policy problem. Management group hierarchies almost always carry inherited policy, so check the hub subscription's effective policy before deploying:
Provider registration is per-subscription. Microsoft.Storage, Microsoft.EventGrid, and Microsoft.ContainerInstance must be registered in the hub subscription. Diagnostic settings additionally require Microsoft.Insights in each target subscription — that's usually already registered, but a fresh subscription in the hierarchy may not have it.
What this costs
The integration itself is free. What you pay for is the Azure infrastructure the template creates:
Storage account, holding activity logs. Flow 1 creates one per subscription; Flow 2 creates one shared account. Usually the largest line item, and it scales with activity volume — a busy production subscription costs more than a dormant one.
Event Grid system topic and subscription, only when
eventDrivenEnabledis true. Priced per operation with the first 100,000 operations each month free, so this is typically negligible.Deployment scripts, which run a container instance and a temporary storage account during deployment only. Cents, once.
Diagnostic settings carry no charge themselves; the cost is the storage they write to.
For most customers the whole thing lands in the low tens of dollars per month. Activity log volume is the variable, and a lifecycle management policy on the storage account is the lever if it grows. Flow 2 is generally cheaper than Flow 1 across the same number of subscriptions, since there's one storage account rather than many.
Verify
Flow 2 first step: read the deployment script output. This is where partial failures live, and neither Azure nor Firefly surfaces them elsewhere.
The script prints an === INTEGRATION SUMMARY === block with total subscriptions found, successful integrations, and a named list of failures with reasons. Compare the total against your Step 1 hierarchy count.
If some subscriptions failed, the fix depends on the reason given. Transient API errors and rate limiting usually clear on a straight re-run of the same deployment, which is safe and idempotent. Authorization errors on specific subscriptions normally mean the management-group role assignments hadn't finished propagating when that subscription was processed, and a re-run resolves those too. A subscription that fails repeatedly is usually blocked by something local to it, such as policy or a disabled state, and is faster to onboard with a targeted Flow 1 run than to keep retrying the whole hierarchy.
Azure-side checks:
Expected role assignments:
Reader
yes
yes
Billing Reader
yes
yes
Security Reader
yes
yes
App Configuration Data Reader
yes
yes
Monitoring Reader
no
yes
Management Group Reader
no
yes
Storage Blob Data Reader (conditional, Terraform state)
yes
hub subscription only
Firefly custom role
yes
hub subscription only
Then in the console: Settings > Integrations > Azure, confirm the expected number of subscriptions is listed and connected, and check Inventory filtered to Azure after 10–15 minutes. If event-driven is on, change a test resource and confirm it surfaces within a couple of minutes.
For a large hierarchy, counting integrations in the console by eye is unreliable. Pull the count from the API instead, using the same key pair the deployment used:
Substitute eu.firefly.ai for EU tenants. That number should match the subscription count from Step 1, less any disabled subscriptions.
Credential rotation
Applies to both flows. Check when the secret expires, at onboarding and periodically thereafter:
To rotate before expiry, add a new secret, redeploy the template with it, then remove the old one:
The redeploy is safe — onConflictUpdate=true updates existing integrations in place rather than creating duplicates. Use --append on the reset so the old secret keeps working until the new one is live; otherwise collection stops between steps 1 and 2.
Flow 2 note: rotation redeploys the whole hierarchy, so it doubles as a resync — any subscriptions added since the last deployment get picked up at the same time.
Troubleshooting
Deployment succeeds, nothing in Firefly
fireflySite wrong for the tenant's region, or an incorrect Firefly key pair. Check the deployment script output
Flow 2: only the hub subscription onboarded
Service principal lacks Management Group Reader. The script fell back silently. The most common management-group failure
Flow 2: fewer integrations than subscriptions
Disabled subscriptions are skipped, or there were per-subscription failures. Read the INTEGRATION SUMMARY block
Flow 2: integration names differ from Azure
Name sanitization stripped unsupported characters. Cosmetic
Flow 2: new subscription not appearing
Expected — registration isn't automatic. Re-run the deployment
Template validation fails on the management-group template
Wrong $schema in the parameter file, or subscriptionIdForDeployment / directoryDomain missing. Both are required with no default
--location missing error
Mandatory on az deployment mg create even though management groups aren't regional
Worked for months, then stopped
Service principal secret expired — see Credential rotation
"Deployment succeeded, script failed"
Azure Policy blocking the deploymentScripts storage account or container instance in the hub subscription
Service principal creation fails
Missing Application Administrator, tenant app registration limit, or a name collision
Insufficient permissions on deploy
Flow 2 needs Owner or User Access Administrator at the management group, not just on subscriptions
Authorization error deploying at the Tenant Root Group
Elevated access not granted — see "Which management group to target"
Activity logs stop arriving after enabling enforceStorageNetworkRules
The storage firewall is denying Azure Monitor. Confirm the storage account permits trusted Azure services and that the Firefly IP allowlist is current
Role assignment errors
servicePrincipalObjectId populated with the app ID instead of the object ID
Terraform state not discovered outside the hub subscription (Flow 2)
Expected — see the permission gap section
Event-driven not firing
Event Grid system topic or subscription missing, or the webhook endpoint doesn't match the tenant's site
Removal
Remove the integrations in the Firefly console first, then clean up Azure.
Flow 1:
Flow 2:
Then, common to both:
Last updated
Was this helpful?