TL;DR
- Ansible can fully automate Azure Entra ID (formerly Azure AD) via the
azure.azcollection: users, groups, app registrations, and even documentation of your Conditional Access Policies. - With Polycrate, all playbooks run in a reproducible container: no local Ansible/Python installation, no version chaos, no "works on my machine" issues – the complete Azure toolchain is defined in the Dockerfile.poly.
- A dedicated Service Principal serves as an automation account – with clearly limited rights (Least Privilege) and securely encrypted credentials in the workspace.
- Every change to Entra ID becomes code: pull requests, reviews, and Git history serve as a technical audit trail for compliance, e.g., in the context of GDPR or internal policies.
- ayedo supports you with Polycrate, best practices, and a specialized Azure automation workshop where we industrialize your Entra ID management together.
Why Automate Entra ID?
For Windows admins and Azure admins, the center of identity management has long shifted from classic on-prem AD to Azure Entra ID. Users, groups, and app registrations are increasingly managed directly there – often still via portal clicks.
This works until one of the following situations occurs:
- Someone spontaneously changes a Conditional Access Policy – and three weeks later, no one remembers why.
- A colleague creates users at night "to get it done quickly" – without a change request, without documentation.
- Onboarding new employees takes time because users and group memberships are still maintained manually.
Automation with Ansible solves exactly these problems:
- All changes are described declaratively.
- Idempotency ensures that the target state can be repeatedly achieved.
- Git workflows provide a traceable change history.
With Polycrate, this automation is not a loose collection of playbooks on multiple laptops but a structured workspace, including encrypted secrets and a reproducible container environment.
Polycrate + Ansible: Clean Environment for Azure Automation
Before we handle users and groups in Entra ID, it's worth looking at the "how."
Containers Instead of Local Ansible Installation
You probably know the classic Ansible problem for Azure:
- Python versions differ among team members.
azure.azcollectionpulls in half an Azure SDK landscape.- One host still has the old Azure CLI, another has none at all.
Polycrate removes this pain:
- Every action runs in the Polycrate container; you optionally extend the image via
Dockerfile.poly– always from the official base image (see below). - You install additional Ansible collections (such as
azure.azcollection) in that layer; Python andansible-corecome from the base image. - The entire team works with the same toolchain – regardless of whether it's a Linux, Mac, or Windows workstation.
Details can be found in the Ansible Integration and the Best Practices.
Installing azure.azcollection in the Polycrate Container
A Dockerfile.poly must always extend the official Polycrate image (cargo.ayedo.cloud/library/polycrate), not a bare python: image: Polycrate relies on the bundled toolchain, mounts, and environment. A plain Python image will not work as the action runtime. See The Polycrate Container.
Install additional collections in a thin layer on top. A minimal example:
FROM cargo.ayedo.cloud/library/polycrate:latest
# requirements.yml for collections (e.g. next to Dockerfile.poly in the workspace)
COPY requirements.yml /workspace/requirements.yml
# Install Azure Collection (Ansible is already in the Polycrate base image)
RUN ansible-galaxy collection install -r /workspace/requirements.yml
WORKDIR /workspace
The corresponding requirements.yml:
---
collections:
- name: azure.azcollection
version: 1.18.0
This makes azure.azcollection available in the Polycrate container for all blocks – no local installation needed.
Workspace and Block for Entra ID Automation
We use the example setup acme-corp-automation and create a dedicated block for Entra ID.
workspace.poly: Include Block
name: acme-corp-automation
organization: acme
blocks:
- name: entra-id-mgmt
from: registry.acme.corp/blocks/entra-id-mgmt:0.1.0
config:
default_domain: "acme-corp.com"
azure_credentials_file: "artifacts/secrets/azure-sp.json"
Important:
from:uses registry-style block notation (<registry>/<path>:<version>).registry.acme.corpis a fictitious example; in practice you usepolycrate blocks pull …or your own OCI registry.workspace.polyandblock.polyhave no Jinja2 –azure_credentials_fileis a literal path to the file underartifacts/secrets/(available decrypted in the container). Ansible playbooks still use{{ block.config.azure_credentials_file }}.- The structure and operation of workspaces are described in the Workspace Documentation.
block.poly: Actions for Users, Groups, Apps, Policies
In the block directory blocks/entra-id-mgmt, we create a block.poly:
name: entra-id-mgmt
version: 0.1.0
kind: ansible
config:
default_domain: ""
azure_credentials_file: ""
actions:
- name: users
description: "Manage users in Entra ID"
playbook: users.yml
- name: groups
description: "Manage groups and memberships"
playbook: groups.yml
- name: apps
description: "Manage app registrations and service principals"
playbook: apps.yml
- name: policies
description: "Document Conditional Access Policies (read-only)"
playbook: policies.yml
Under config: you only list the expected keys (here with empty defaults); there is no Jinja2 in .poly files. Set concrete values on the block instance in workspace.poly (see above).
This provides clear guardrails:
- Each task is a clearly named action.
- No loosely growing collection of playbooks.
- For your team, a simple UX emerges:
polycrate run entra-id-mgmt usersinstead of complex Ansible CLI calls.
More about blocks and actions can be found under Blocks and Actions.
Service Principal as Automation Account
For Entra ID automation, we need a dedicated Service Principal.
Roles and Least Privilege
Depending on what you automate, you need different Entra roles. A common setup:
- User Management: Role
User Administrator - Group Management: Role
Groups Administrator - App Registrations: Role
Application Administratoror more finely grained app registration permissions
Best Practice:
- Create a separate "Automation" Service Principal.
- Assign only the minimally necessary roles.
- Use credentials exclusively in the Polycrate workspace (do not leave them lying around locally).
Store Credentials in a Secret
Create the file artifacts/secrets/azure-sp.json in the workspace:
{
"client_id": "00000000-0000-0000-0000-000000000000",
"client_secret": "SUPER-SECRET-VALUE",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"subscription_id": "22222222-2222-2222-2222-222222222222"
}
Configure this file via secrets.poly as a secret to be encrypted (details in the Workspace Encryption) and then encrypt the workspace:
polycrate workspace encrypt
With this:
- The file is encrypted in the Git repository.
- Polycrate provides it in the container at the configured path (e.g.
artifacts/secrets/azure-sp.json). - You don't need to operate an external tool like Vault.
Managing Users with azure_rm_aduser
Now it gets concrete: We manage Entra ID users with azure.azcollection.azure_rm_aduser.
users.yml: Create, Disable, Delete Users
The file blocks/entra-id-mgmt/users.yml:
---
- name: Manage users in Entra ID
hosts: localhost
connection: local
gather_facts: false
vars:
azure_credentials_file: "{{ block.config.azure_credentials_file }}"
default_domain: "{{ block.config.default_domain }}"
tasks:
- name: Load Azure Service Principal Credentials
ansible.builtin.include_vars:
file: "{{ azure_credentials_file }}"
name: azure
- name: Create or update user
azure.azcollection.azure_rm_aduser:
state: present
user_principal_name: "alice@{{ default_domain }}"
display_name: "Alice Example"
password: "InitialPassw0rd!"
account_enabled: true
force_password_change: true
tenant: "{{ azure.tenant_id }}"
client_id: "{{ azure.client_id }}"
secret: "{{ azure.client_secret }}"
auth_source: client_secret
register: alice_result
- name: Disable user (example)
azure.azcollection.azure_rm_aduser:
state: present
user_principal_name: "bob@{{ default_domain }}"
display_name: "Bob Example"
account_enabled: false
tenant: "{{ azure.tenant_id }}"
client_id: "{{ azure.client_id }}"
secret: "{{ azure.client_secret }}"
auth_source: client_secret
- name: Delete user (example)
azure.azcollection.azure_rm_aduser:
state: absent
user_principal_name: "temp.user@{{ default_domain }}"
tenant: "{{ azure.tenant_id }}"
client_id: "{{ azure.client_id }}"
secret: "{{ azure.client_secret }}"
auth_source: client_secret
- name: Output brief summary
ansible.builtin.debug:
msg: "User management completed. Alice change: {{ alice_result.changed }}"
Important:
hosts: localhostandconnection: localare correct here – the playbook runs in the Polycrate container and communicates with the Azure APIs, not an SSH host.- The credentials are loaded from the encrypted file and not stored in the
block.polyor the playbook.
Executing the action:
polycrate run entra-id-mgmt users
With plain Ansible, you would need to ensure that locally:
- Python, Ansible,
azure.azcollection, and all dependencies are installed. - The paths to credential files are correct on every admin laptop.
With Polycrate, it's enough to clone the workspace, use polycrate run, and everything runs in the defined container.
Managing Groups and Memberships with azure_rm_adgroup
Groups are the heart of permissions in Entra ID. The principle is the same: declaratively describe which groups exist and which members belong to them.
groups.yml: Groups and Members
```yaml
- name: Manage groups in Entra ID hosts: localhost connection: local gather_facts: false
vars: azure_credentials_file: "{{ block.config.azure_credentials_file }}" default_domain: "{{ block.config.default_domain }}"
tasks: - name: Load Azure Service Principal Credentials ansible.builtin.include_vars: file: "{{ azure_credentials_file }}" name: azure
- name: Ensure security group "HR-Staff"
azure.azcollection.azure_rm_adgroup:
state: present
display_name: "HR-Staff"
mail_nickname: "hr-staff"
security_enabled: true
description: "HR Staff"
tenant: "{{ azure.tenant_id }}"
client_id: "{{ azure.client_id }}"
secret: "{{ azure.client_secret }}"
auth_source: client_secret
register: hr_group
- name: Maintain members in HR-Staff
azure.azcollection.azure_rm_adgroup:
state: present
display_name: "HR-Staff"
security_enabled: true
members:
# Example UPNs, better to use Object IDs in practice
- "alice@{{ default_domain }}"
tenant: "{{ azure.tenant_id }}"
client_id: "{{ azure.client_id }}"
secret: "{{ azure.client_secret }}"
auth_source: client_secret