Automating Active Directory: Users, Groups, and OUs with Ansible
Fabian Peter 11 Minuten Lesezeit

Automating Active Directory: Users, Groups, and OUs with Ansible

Automating Active Directory: Users, Groups, and OUs with Ansible and Polycrate

TL;DR

  • Active Directory changes via GUI or non-versioned PowerShell scripts are error-prone, hard to track, and not very collaborative. With Ansible and Polycrate, they become reproducible, versioned, and team-friendly.
  • We build a Polycrate block for AD management that handles users, groups, and OUs using the modules community.windows.win_domain_user, community.windows.win_domain_group, and community.windows.win_domain_ou—including CSV-based bulk onboarding and offboarding playbook with audit log.
  • Each run leaves traces in the Git repository (CSV, playbooks, audit logs) and is automatically committed with polycrate workspace sync: A clean audit trail for compliance requirements like GDPR, effective since 25.05.2018.
  • Polycrate encapsulates Ansible completely in a container, including all Windows/AD dependencies; no more Python/Ansible chaos on admin laptops and clearly structured blocks instead of playbook sprawl.
  • ayedo supports you in establishing such AD automations as reusable, compliance-capable building blocks in your organization—from conception to integration into existing processes.

Why AD Automation Has Been So Tedious

If you manage a classic Active Directory today, the daily routine often looks like this:

  • Users are manually created in the MMC
  • Group memberships are “just adjusted”
  • Occasionally, PowerShell scripts exist—but without central versioning, without tests, often only on an admin laptop

This works as long as you have few changes and the same two people do everything. As soon as teams grow, audits come in, or multiple locations arise, it becomes difficult:

  • No Traceability: Who changed which group when? Why is this user in the “Domain Admins” group?
  • No Audit Trail: For compliance (e.g., GDPR, internal policies, ISO 27001), you need traceable changes—GUI clicks are hard to prove.
  • Script Sprawl: Everyone writes their own PowerShell scripts, with minimal variations. Errors are hardly reproducible.
  • Dependency Chaos: The PowerShell script from a colleague doesn’t run on your machine because modules are missing or versions don’t match.

With plain Ansible, some things improve—you get idempotency, understandable YAML playbooks, and a powerful ecosystem. But:

  • You must cleanly install Ansible, Python, and the appropriate collections (e.g., community.windows) on each machine.
  • Versions differ from admin to admin.
  • Shared playbooks end up “somewhere” in Git without clear structure—the next sprawl.

This is where Polycrate comes in.


Polycrate + Ansible: Structured AD Management with Audit Trail

Polycrate turns your Ansible playbooks into versioned, shareable blocks and solves several typical problems:

  • Dependency Problem Solved: Ansible always runs in a Polycrate container. The required collections (e.g., community.windows) are defined in the container image. No local Ansible, no Python chaos, less supply chain risk. More on the Ansible integration of Polycrate.
  • Sharable Automation: Your AD block is a standalone artifact that you can share in an OCI registry or on PolyHub. Teams bind it in with a full registry reference in from: (see the workspace example below).
  • Guardrails Instead of Playbook Sprawl: The block model clearly separates actions (onboarding, offboarding, OU setup). No more “everything in one huge playbook”.
  • Compliance-ready:
    • Git versioning of the entire workspace, including audit logs.
    • Automatic commits via polycrate workspace sync (see Git integration of Polycrate).
    • Workspace encryption with age to securely store passwords and inventories (see Workspace Encryption).

We will now build a workspace with an AD management block that:

  • Creates OUs
  • Imports users from a CSV file
  • Creates groups and maintains members
  • Automates offboarding with deactivation, group removal, and audit log

Preparing the Workspace for AD Management

First, we create the workspace acme-corp-automation and define our AD block and inventory.

workspace.poly

name: acme-corp-automation
organization: acme

blocks:
  - name: ad-mgmt
    from: registry.acme-corp.com/acme/infra/ad-mgmt:1.0.0
    config:
      domain_name: "acme-corp.com"
      default_ou: "OU=Users,DC=acme-corp,DC=com"

from: holds the full OCI registry reference including the version tag at the end of the line (same idea as container images); there is no separate version field under the block instance. The block package’s block.poly uses the same logical name (ad-mgmt). The registry registry.acme-corp.com is a fictional example (not ayedo’s product registry). If the block is missing locally, Polycrate can pull it on the first polycrate run …; unpacked it lives under blocks/registry.acme-corp.com/acme/infra/ad-mgmt/. See also Multi-server management with Ansible inventories.

More about workspaces: Polycrate Workspaces.

Inventory for the Domain Controller

Polycrate expects a YAML inventory in the workspace root as inventory.yml. All hosts live under all.hosts; under children, the domain_controllers group references them by name only—shared WinRM settings go in the group vars. No passwords in the inventory—set ansible_password in playbooks from secrets.poly / workspace.secrets.* (see Workspace Encryption). For a domain controller dc01.acme-corp.com, it might look like this:

all:
  hosts:
    dc01.acme-corp.com: {}

  children:
    domain_controllers:
      hosts:
        dc01.acme-corp.com:
      vars:
        ansible_user: Administrator
        ansible_connection: winrm
        ansible_port: 5986
        ansible_winrm_transport: credssp
        ansible_winrm_server_cert_validation: ignore

Important:

WinRM password for playbooks: Store e.g. ad_dc_password in secrets.poly; in every play against domain_controllers, add under vars: ansible_password: "{{ workspace.secrets.ad_dc_password }}" (the examples below include this).


Defining the AD Block: block.poly

Our block blocks/registry.acme-corp.com/acme/infra/ad-mgmt/block.poly (after pulling from the registry) encapsulates all AD activities:

name: ad-mgmt
version: 0.1.0
kind: generic

config:
  domain_name: "acme-corp.com"
  default_ou: "OU=Users,DC=acme-corp,DC=com"
  users_csv: "users.csv"
  audit_log: "audit.log"

actions:
  - name: setup_ou_and_groups
    playbook: setup_ou_and_groups.yml

  - name: onboarding
    playbook: onboarding.yml

  - name: offboarding
    playbook: offboarding.yml
  • config.* values can be overridden in the workspace if needed.
  • Playbooks live in the block directory (registry path under blocks/…). Runtime data such as CSV and audit log do not go inside the block; place them under artifacts/blocks/<block-instance-name>/ in the workspace—e.g. artifacts/blocks/ad-mgmt/users.csv. In Ansible, block.artifacts.path points to that directory (in the container: /workspace/artifacts/blocks/<name>/).

Further details on blocks can be found in the Polycrate Blocks Docs.


CSV-based Bulk Onboarding of AD Users

Example CSV for Users

Create the file in the workspace at artifacts/blocks/ad-mgmt/users.csv (not under the pulled block in blocks/…). Polycrate exposes the path in playbooks as {{ block.artifacts.path }} (same as artifacts/blocks/ad-mgmt/ in the workspace).

username;givenName;sn;password;enabled;ou_dn;groups
jdoe;John;Doe;Start123!;true;OU=IT,OU=Users,DC=acme-corp,DC=com;IT-Admins,VPN-Users
mroe;Maria;Roe;Start123!;true;;Sales,VPN-Users
  • ou_dn is optional—if missing, we use default_ou.
  • groups is a comma-separated list.

Playbook: Onboarding with win_domain_user and win_domain_group

The onboarding consists of two plays:

  1. Read CSV in the Polycrate container (localhost, allowed because only files are read)
  2. Maintain users and groups on the domain controller
# blocks/registry.acme-corp.com/acme/infra/ad-mgmt/onboarding.yml
- name: Load users from CSV in Polycrate container
  hosts: localhost
  gather_facts: false
  vars:
    users_csv: "{{ block.artifacts.path }}/{{ block.config.users_csv }}"
  tasks:
    - name: Read users from CSV
      community.general.read_csv:
        path: "{{ users_csv }}"
        delimiter: ";"
      register: csv_users

    - name: Normalize user list
      ansible.builtin.set_fact:
        ad_users: "{{ csv_users.list }}"

- name: Create OUs and groups on domain controllers
  hosts: domain_controllers
  gather_facts: false
  vars:
    ansible_password: "{{ workspace.secrets.ad_dc_password }}"
    domain_name: "{{ block.config.domain_name }}"
    default_ou: "{{ block.config.default_ou }}"
  tasks:
    - name: Ensure base OU exists
      community.windows.win_domain_ou:
        name: "{{ default_ou }}"
        state: present

    - name: Ensure department OUs from CSV exist
      community.windows.win_domain_ou:
        name: "{{ item.ou_dn }}"
        state: present
      loop: "{{ hostvars['localhost'].ad_users | map(attribute='ou_dn') | list | unique }}"
      when: item.ou_dn is defined and item.ou_dn | length > 0

    - name: Collect all group names from CSV
      ansible.builtin.set_fact:
        all_groups: >-
          {{ hostvars['localhost'].ad_users
             | map(attribute='groups')
             | select('defined')
             | map('split', ',')
             | sum(start=[])
             | map('trim')
             | reject('equalto', '')
             | list }}
      run_once: true

    - name: Ensure groups exist
      community.windows.win_domain_group:
        name: "{{ item }}"
        scope: global
        state: present
      loop: "{{ all_groups | default([]) }}"
      run_once: true

    - name: Ensure AD users exist and are in the right groups
      community.windows.win_domain_user:
        name: "{{ item.username }}"
        upn: "{{ item.username }}@{{ domain_name }}"
        given_name: "{{ item.givenName }}"
        surname: "{{ item.sn }}"
        password: "{{ item.password }}"
        enabled: "{{ (item.enabled | default('true')) | bool }}"
        path: "{{ item.ou_dn | default(default_ou) }}"
        state: present
        groups: >-
          {{ (item.groups | default(''))
             | split(',')
             | map('trim')
             | reject('equalto', '')
             | list }}
      loop: "{{ hostvars['localhost'].ad_users }}"
      run_once: true

What happens here?

  • CSV Reading in the Container: community.general.read_csv runs on localhost in the Polycrate container. It does not modify target systems, so hosts: localhost is correct here.
  • OU Management: With community.windows.win_domain_ou, we first create a base OU and then possibly additional OUs from the CSV data.
  • Group Management: community.windows.win_domain_group creates all groups referenced in the CSV.
  • User Management: community.windows.win_domain_user creates or updates users idempotently, including OU, password, status, and groups.

With plain Ansible without Polycrate, you would need to:

  • Install Ansible and all collections locally.
  • Ensure that community.windows is equally available everywhere.
  • Manually link playbooks and CSV with Git.

With Polycrate, a structured block and a reproducible container are sufficient.

Execute Onboarding

From the workspace root:

cd ~/workspaces/acme-corp-automation
polycrate run ad-mgmt onboarding

Polycrate takes care of:

  • Starting the container with the correct Ansible and collection version
  • Providing the workspace in the container
  • Passing the inventories

Offboarding Playbook with Audit Log

For offboarding, you want to:

  • Disable users
  • Remove from groups
  • Optionally move to a “Disabled Users” OU
  • Write a traceable audit entry

Offboarding Playbook

# blocks/registry.acme-corp.com/acme/infra/ad-mgmt/offboarding.yml
- name: Disable AD user and clean up group memberships
  hosts: domain_controllers
  gather_facts: false
  vars:
    ansible_password: "{{ workspace.secrets.ad_dc_password }}"
    domain_name: "{{ block.config.domain_name }}"
    disabled_ou: "OU=Disabled Users,DC=acme-corp,DC=com"
    username: "{{ username }}"
  tasks:
    - name: Ensure OU for disabled users exists
      community.windows.win_domain_ou:
        name: "{{ disabled_ou }}"
        state: present

    - name: Disable user and move to disabled OU
      community.windows.win_domain_user:
        name: "{{ username }}"
        upn: "{{ username }}@{{ domain_name }}"
        enabled: false
        path: "{{ disabled_ou }}"
        state: present

    - name: Remove user from sensitive groups
      community.windows.win_domain_group:
        name: "{{ item }}"
        state: present
        members:
          - "{{ username }}"
        members_state: absent
      loop:
        - "IT-Admins"
        - "Domain Admins"
        - "VPN-Users"

- name: Write audit log entry in Polycrate workspace
  hosts: localhost
  gather_facts: false
  vars:
    audit_log: "{{ block.artifacts.path }}/{{ block.config.audit_log }}"
  tasks:
    - name: Append audit log entry
      ansible.builtin.lineinfile:
        path: "{{ audit_log }}"
        line: "{{ lookup('ansible.builtin.pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }};{{ username }};offboarded"
        create: yes
        insertafter: EOF

Notes:

  • The first play runs on domain controllers and uses Windows-specific modules.
  • The second play runs in the Polycrate container (localhost) and writes to audit.log under {{ block.artifacts.path }} (workspace: artifacts/blocks/ad-mgmt/). That file is part of the workspace and is versioned and encrypted with it.

Run offboarding for a user

Pass the user via an extra variable:

cd ~/workspaces/acme-corp-automation
polycrate run ad-mgmt offboarding -- -e "username=jdoe"

The -- separator splits Polycrate arguments from those passed to ansible-playbook.


Versioning changes and securing the audit trail

The real compliance strength comes from combining:

  • Playbooks + CSV + audit logs in Git
  • Automated commits with Polycrate

After an onboarding or offboarding run:

  • users.csv may have changed (e.g. new users).
  • audit.log may have a new entry.
  • Your playbooks and block definition are unchanged or updated.

Keep the workspace in sync with Git, for example:

cd ~/workspaces/acme-corp-automation
polycrate workspace sync

Polycrate:

  • Detects changes in the workspace.
  • Creates a commit with a sensible message.
  • Optionally pushes to your central Git repository (depending on configuration).

Details: Git integration of Polycrate.

The compliance advantage over GUI clicks or loose PowerShell:

  • Every AD change is traceable via playbooks and CSV data.
  • Every run leaves a commit with timestamp and diff.
  • audit.log adds a human-readable overview (“when was which user offboarded?”).

That is gold in audits or forensic analysis.


Share the block and standardize across the company

You develop the block in the workspace under the registry-compatible directory that matches your from: reference—for example blocks/registry.acme-corp.com/acme/infra/ad-mgmt/ (registry host and path as in the OCI reference without the version tag), not a short path like ./blocks/ad-mgmt/. Then you publish it to your OCI registry or PolyHub—other workspaces consume it with a full from: reference and version tag, as in the first workspace.poly example. The same directory layout applies to the Polycrate cargo registry (blocks/cargo.ayedo.cloud/…). Alternatively (or in addition), use cargo in from:—for example:

blocks:
  - name: ad-mgmt
    from: cargo.ayedo.cloud/acme/infra/ad-mgmt:0.1.0
    config:
      domain_name: "acme-corp.com"
      default_ou: "OU=Users,DC=acme-corp,DC=com"

This means:

  • Other teams use the exact same, tested AD block.
  • You can roll out new versions (e.g. 0.2.0) without breaking existing workspaces.
  • Automation becomes an organization-wide standard building block, not a pile of individual scripts.

More: Registry and PolyHub.


Frequently asked questions

Do I still need Ansible or extra Python packages on my admin laptop?

No. Polycrate ships a container image with Ansible, Python, and required collections such as community.windows already installed. You only run polycrate run ...; Polycrate starts the container, mounts the workspace, and runs ansible-playbook inside it.

Benefits:

  • Every admin uses the same toolchain—no version conflicts.
  • Less risk that someone “quickly” updates a module on a laptop and breaks playbooks.
  • A clear supply chain: the container image is your defined control node.

How do I protect passwords and sensitive data in the workspace?

The workspace can be encrypted with age via polycrate workspace encrypt. That lets you store files such as:

  • secrets.poly and encrypted artifacts (AD credentials, no plaintext passwords in inventory.yml)
  • CSV files with temporary initial passwords
  • additional secret files

encrypted in the Git repository. Only people with the right keys can decrypt the workspace.

Details: Workspace encryption.

Can I use the AD block across multiple domains or environments (e.g. test/prod)?

Yes. Via blocks.config in workspace.poly you can set different parameters per workspace (or environment), e.g.:

  • domain_name
  • default_ou
  • different CSV files

For test and production, create separate workspaces that reference the same block from the registry but use different configuration and inventories. The logic stays the same while targets and data vary per environment.

More questions? See our FAQ.


Compliance in practice

With the AD block shown here you have taken an important step:

  • AD users, groups, and OUs are described declaratively, not clicked by hand.
  • Bulk onboarding from CSV, offboarding with deactivation and group cleanup—traceable and repeatable.
  • Git history, polycrate workspace sync, and the audit log create auditable evidence for audits, pen tests, or internal controls.
  • Polycrate removes operational load: no local Ansible install, no uncontrolled scripts, clear structure through blocks.

How ayedo can help

We help organizations build automations like this systematically—from the first AD action to whole landscapes of standardized Polycrate blocks. Whether you want to improve onboarding/offboarding only or establish broader policy-as-code for Active Directory, we bring platform engineering, Windows, and compliance experience together.

If you want to explore how this could look in your environment and which extensions (e.g. automatic GPO mapping or HR integration) make sense, a joint review of your requirements is a good next step:

AD management demo

Ähnliche Artikel