Azure for AWS engineers: how your AWS instincts translate (and where they break)
You already know how to organize accounts, hand out permissions, and bootstrap state in AWS. Here's how to do the same things in Azure β the concepts that map cleanly, and the four places your AWS muscle memory will actively mislead you.
If you're fluent in AWS and now staring at Azure, the good news is that ~80% of your instincts transfer directly: there's a hierarchy to organize things, a role-based way to grant access, an OIDC story for keyless CI, and a "cold-start your state backend" bootstrap dance. The bad news is the other 20% β and it's concentrated in a few high-traffic spots where doing the AWS thing produces a confusing denial rather than an obvious error. This post is the translation layer: the same jobs you do in AWS, done in Azure, with the traps called out.
The short version, if you only read one paragraph: AWS has essentially one permission plane (IAM), Azure has three that don't talk to each other; AWS accounts are Azure subscriptions but billing lives somewhere else entirely; and Azure adds a mandatory container (the resource group) that AWS has no real equivalent for. Everything below expands those.
The biggest shift: one permission plane becomes three
In AWS, IAM is effectively the whole story. One service governs who your identities are, what they can do to resources, and β through Organizations β how accounts are created and grouped. Permissions bleed together in a way you probably don't even notice until it's gone.
Azure deliberately splits that single plane into three separate planes, with three role systems, three grant mechanisms, and almost no automatic bleed-through. Being all-powerful in one plane grants you nothing in the others. This is the single most important thing to internalize, because it's the source of nearly every "but I'm an admin, why is this denied?" moment.
The rule that saves you: when Azure denies something that "should" work, first ask "which plane am I talking to?" β then check that plane's roles. Most mysterious denials (can't read a subscription, can't list management groups, can't see billing) aren't a missing permission within a plane β they're you talking to the wrong plane entirely.
Plane 1 β Entra ID directory roles (identity)
This plane governs directory objects: users, groups, service principals / app registrations, Conditional Access, MFA policy, licenses. It does not govern the resources you deploy.
- Roles here are things like Global Administrator, User Administrator, Application Administrator. They're assigned and evaluated in Entra ID and surfaced through the Microsoft Graph API.
- Global Administrator is a directory god, not a resource god. This trips up everyone: a Global Admin can happily manage every user and app in the org and still get an authorization failure trying to merely read a subscription. The closest AWS analogy is "the person who administers IAM Identity Center and the directory itself" β but it's imperfect, precisely because AWS fuses directory administration with resource authorization and Azure refuses to.
Plane 2 β Azure RBAC (resources)
This is the plane you'll live in, and the one the azurerm Terraform provider drives. It governs everything you deploy:
VMs, virtual networks, storage, AKS, and so on.
- A grant is a triple: (principal, role definition, scope), where scope is a node in the chain
root β management group β subscription β resource group β resource, and it inherits downward. Built-in roles are Owner, Contributor, Reader, plus any custom role definitions you write. - Here's the trap for AWS brains: there are no identity-attached policies. In AWS you attach a policy to a user or role and the permission travels with the identity. In Azure, role-at-a-scope is the only model. The closest AWS mental shape is "an IAM policy attached to an Organizations OU" β the grant lives on the tree node, not on the principal.
- The one sanctioned bridge between planes 1 and 2 is a deliberate break-glass move: a Global Administrator can toggle
"Access management for Azure resources" (the
elevateAccessoperation) to grant themselves User Access Administrator at the root scope. It's loud, reversible, and not a default β you use it once during bootstrap to assign yourself Owner at the tenant root, which then inherits into every subscription.
Plane 3 β billing / commerce (money)
This plane governs billing accounts, billing profiles, invoice sections, payment methods β and, critically, subscription creation.
- It has its own role set (Billing account owner, Billing profile owner, Azure subscription creator, β¦), granted
inside the billing account and stored in the billing system. These roles do not show up in
az role assignment listor the Entra role blades. They're a completely separate world. - You can hit this wall from both directions. Maximal identity + resource rights (Global Admin + User Access
Administrator at root + Owner at the root management group) still gets you an empty list from
az billing account listand no ability to create a subscription. Conversely, a billing owner can grant billing rights with one click. - The sneakiest part: billing is served through ARM-looking REST URLs (
Microsoft.Billing/...), so it looks like the resource plane β but authorization is evaluated against billing roles. Same door, different bouncer.
A concrete consequence AWS never makes you think about: whether you can create subscriptions programmatically at all
depends on your billing agreement type. Legacy pay-as-you-go / web-direct accounts can only create subscriptions
manually in the portal, by the original signup identity β that ownership isn't even grantable. The modern Customer
Agreement supports a subscription-creation API (with retail caps on self-serve accounts β a handful of subscriptions
total, and a rate limit per day), and enterprise/partner agreements lift the caps. In AWS, CreateAccount just works
from the management account; in Azure, "can I create this account with code?" is a billing-plane question you have to
answer first. That's why many Azure estates treat subscriptions as imported into Terraform, never created by it.
How the planes intersect in practice
Most tasks touch exactly one plane, and a few span several β which is the whole reason the split matters:
- Create a user, group, or app registration β Entra only.
- Deploy a VM / VNet / storage account β resources (Azure RBAC) only.
- Create a subscription β billing creates it, it homes into an Entra tenant, and it becomes an Azure RBAC scope. Three planes for one action.
- Export directory audit logs to a Log Analytics workspace β Entra (source) plus resources (destination).
- Terraform
azurermvsazureadβ the resource API vs the Graph API β different endpoints, different token audiences.
That last point has a real operational bite: a single login mints separate tokens per audience (one for the resource manager, one for Graph, one for Key Vault). A token minted for one plane's API is worthless at another's. If your tooling only grabs one, half your Terraform will mysteriously 401.
"Entra ID," "tenant," and "directory" are (mostly) the same thing
Three words for one object seen from different angles, plus a rename to trip you up:
- Entra ID is the product β the identity service. Until 2023 it was Azure Active Directory (Azure AD / AAD),
and the old name is everywhere: the
azureadTerraform provider,AADSTSβ¦error codes, "AAD auth" in docs. Same thing. - A tenant is your organization's dedicated instance of Entra ID β the container and the trust/isolation boundary, identified by a GUID and a primary domain. Users, groups, apps, Conditional Access, and licenses all live per tenant and don't cross tenants.
- The directory is the contents of a tenant β the database of identity objects. One tenant equals one directory, so people use the words interchangeably; the portal's "Switch directory" button actually switches tenants.
The rules that follow are where the AWS analogy gets loose:
- A subscription homes to exactly one tenant. That tenant is its authentication realm and supplies its RBAC principals. (Subscriptions can be transferred between tenants; billing is a separate association.)
- One organization can own many tenants. A common pattern is a locked-down production tenant plus a fully isolated sandbox tenant β separate identity worlds where nothing you do in one can touch the other. There's no AWS equivalent to "a second, completely separate identity universe under the same company."
- Users have one home tenant and can be guests (B2B) elsewhere. A guest gets a local object ID in the guest tenant while keeping their home identity. AWS has no first-class notion of "a guest user from another organization's directory."
The loosest AWS analogy: a tenant is roughly "an AWS Organization fused with its Identity Center directory" β except subscriptions attach to a tenant only for identity, while payment attaches to a billing account, and cross-org guests are a native concept.
Resource groups: the container AWS doesn't have
A resource group (RG) is a mandatory container inside a subscription β every single resource lives in exactly one. This is the piece with no AWS equivalent (AWS "resource groups" are just saved tag queries; ignore the name collision).
An RG is four things at once:
- An RBAC scope β grant Reader on one RG and you've scoped access to exactly that workload.
- A policy scope β attach governance rules at the RG level.
- A lifecycle unit β delete the RG and everything in it goes with it.
- A cost boundary β a natural line item for spend.
The idiomatic pattern is one RG per workload (often per region), so an RG becomes "the folder for this app in this region." An RG has a location, but that only says where its metadata lives β its resources can sit in other regions, so don't over-index on RG location.
Resource IDs carry context, so names don't have to
Every Azure resource has a full Resource ID like
/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.App/containerApps/<name>. The subscription and RG travel
with every log record, audit event, and API call β the same job an ARN's account and region do in AWS.
The naming consequence is the opposite of your AWS habit. In AWS the name is often the only context you get, so you
cram everything into it. In Azure, names should omit what the scope already encodes: a cluster named aks-quest
inside rg-quest inside the qa subscription is already fully disambiguated, and the same RG/resource names can safely
repeat across dev/qa/prod subscriptions. Only globally unique resource types (storage accounts, container registries,
Key Vaults) force org/env/region back into the name β and those come with strict length and character limits, which is
why you see squashed names like stcwtfstateplatformcus.
(That cus suffix isn't invented, by the way β it's Microsoft's own geo-code for Central US, from the same official
table Azure uses to build private-endpoint DNS zones. eus/eus2 are East US and East US 2. Learning the geo-code
table early pays off.)
The AWS β Azure quick map
Keep this next to you for the first month:
- Organization β an Entra tenant plus management groups. Identity (the tenant) and structure (management groups) are separate things in Azure, not one.
- Account β subscription. Homes to a tenant for identity; billed by a separate billing account.
- SCP (guardrail) β Azure Policy at a management-group scope β with richer effects than deny-only (audit, deny, modify, deployIfNotExists).
- IAM role/policy β RBAC role assignment β (principal, role, scope). Remember: no identity-attached policies.
- Root-account superpowers β split three ways: Global Admin (identity) + elevateAccess (resources) + billing owner (money). No single principal starts with all three.
- Organizations
CreateAccountβ the subscription aliases API + a billing scope β and only on the billing agreement types that allow it. - IRSA (IAM Roles for Service Accounts) β workload identity federation β the same OIDC idea.
- Tag-based grouping β the resource group β structural and mandatory, not a tag query.
- ARN in logs β the Resource ID (
_ResourceIdcolumn) β scope context is structured, not name-encoded.
Bootstrapping Terraform state, translated
Here's a place where your AWS runbook almost works, then breaks at step zero.
The AWS bootstrap you know: cold-start a state backend in the management account (local state, then migrate the backend into itself), keep the per-OU backends' state in that root backend, and create member accounts using their own backends. The invariant that makes it clean is that you can always bootstrap from the root account, because it always exists and can hold an S3 bucket.
Azure breaks that invariant at genesis. The always-exists object is the tenant β but a tenant can't hold resources. Storage accounts live in subscriptions, and subscriptions are born from the billing plane. So Azure's "root account" is whatever subscription you crown as root, and you have to crown one consciously. In practice most tenants get one subscription at signup, so the parallel mostly holds: AWS hands you a management account, Azure hands you a first subscription.
The translated flow, when subscriptions already exist (the common case):
- One cold-start, ever: deploy the state backend into your designated root subscription with local state, then
init -migrate-stateinto itself. - Per-subscription backends, state stored in the root backend: each additional subscription's backend component pins its own state to the root backend, so standing them up is a normal apply β no more cold-starts.
- Everything else in each subscription uses that subscription's own backend.
From absolute scratch (creating subscriptions with code), the ordering is forced β root subscription first, root backend
second β because a backend is a storage account and a storage account needs a subscription to live in. Note one
chicken-and-egg wrinkle: the azurerm provider itself demands a subscription context, so with zero subscriptions you
make that very first subscription with an imperative call (the CLI or the azapi provider), then import it into
Terraform afterward.
And here's where Azure is genuinely simpler than AWS muscle memory: backend-in-root plus resources-in-member needs
none of the AWS hub-and-spoke trust machinery β no backend role_arn, no provider assume_role, no two-sided trust
policies. Owner at the tenant root management group inherits into every subscription, so one token works tenant-wide;
the provider "hops" between subscriptions by simply setting subscription_id; and the backend is just data-plane blob
access authorized by a Storage Blob Data Contributor grant. Cross-subscription is a parameter, not a trust
negotiation. (You'll tighten this later with custom roles, PIM, and dedicated CI identities β but even then it's role
assignments at scopes, never a trust-policy handshake.) If you want to go deep on codifying either side,
the HashiCorp Terraform Authoring & Operations Pro exam is built around exactly these state and
provider patterns.
The four places your AWS instincts will actively mislead you
If you forget everything else, remember these:
- "Admin" is not global. Global Administrator is identity-only; it can't read a subscription until someone grants it a resource role. There is no single "root" principal.
- Permissions attach to scopes, not identities. Stop looking for the policy on the user β look for the role assignment on the management group, subscription, resource group, or resource.
- Billing is a separate universe from resources. "I can deploy anything" tells you nothing about "I can create a
subscription." Different plane, different roles, invisible to
az role assignment list. - The resource group is load-bearing. It's not a tag β it's an RBAC scope, a policy scope, and a delete boundary. Design your RG layout on purpose.
Which certifications drill each side
Cloud governance β hierarchy, identity, guardrails, and state β is the backbone of the architecture and administration exams on both clouds, so studying for them is also the fastest way to make the concepts above stick. If you already hold the AWS credential, the Azure one on the same row is your natural next step.
On the AWS side (the knowledge you're translating from):
- AWS Certified Cloud Practitioner (CLF-C02) β the foundations of accounts, IAM, and Organizations.
- AWS Certified Solutions Architect β Associate (SAA-C03) β IAM, multi-account structure, and core architecture.
- AWS Certified Solutions Architect β Professional (SAP-C02) β multi-account Organizations, SCPs, and cross-account access at scale.
- AWS Certified Security β Specialty (SCS-C03) β IAM depth, SCPs, and key management.
On the Azure side (the knowledge you're translating to):
- Microsoft Certified: Azure Fundamentals (AZ-900) β tenants, subscriptions, resource groups, and the basics of RBAC.
- Microsoft Certified: Azure Administrator Associate (AZ-104) β the day-to-day plane: RBAC, resource groups, subscriptions, and Entra basics.
- Microsoft Certified: Azure Solutions Architect Expert (AZ-305) β management groups, governance, and landing-zone design.
- Microsoft Certified: Azure Security Engineer Associate (AZ-500) β Entra ID, RBAC, Azure Policy, Conditional Access, and Key Vault.
A pragmatic path if you're AWS-certified today: start at AZ-900 to map the vocabulary, then jump to AZ-104 (which lives in the resource plane you'll use most), and add AZ-305 or AZ-500 depending on whether your work leans architecture or security.
The bottom line
Azure isn't harder than AWS β it's factored differently. AWS collapses identity, resource, and billing authority into one plane and hands you a management account that can do everything. Azure separates those three concerns on purpose, which feels like friction on day one and like clean boundaries by month three. Translate the concepts once β one plane becomes three, accounts become subscriptions billed elsewhere, policies attach to scopes instead of identities, and the resource group is real structure β and the rest is vocabulary. Drill it with the certs above, and the parts that felt like arbitrary denials start reading as a coherent, deliberate design.