Automating Azure Entra ID: Users, Groups, and Apps with Ansible
Fabian Peter 7 Minuten Lesezeit

Automating Azure Entra ID: Users, Groups, and Apps with Ansible

Automate Azure Entra ID: Users, Groups, and App Registrations with Ansible and Polycrate

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.azcollection pulls 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 and ansible-core come 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.corp is a fictitious example; in practice you use polycrate blocks pull … or your own OCI registry.
  • workspace.poly and block.poly have no Jinja2 – azure_credentials_file is a literal path to the file under artifacts/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 users instead 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 Administrator or 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: localhost and connection: local are 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.poly or 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

---
- 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

Ähnliche Artikel