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, andcommunity.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:
- This inventory is how Polycrate reaches Windows hosts over WinRM (see Ansible documentation and the pattern in Multi-server management with Ansible inventories).
- Passwords and other secrets belong in
secrets.polyand are wired in playbooks as variables—not as plaintext ininventory.yml. - Protect the whole workspace with
polycrate workspace encrypt(see Workspace Encryption).
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 underartifacts/blocks/<block-instance-name>/in the workspace—e.g.artifacts/blocks/ad-mgmt/users.csv. In Ansible,block.artifacts.pathpoints 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_dnis optional—if missing, we usedefault_ou.groupsis a comma-separated list.
Playbook: Onboarding with win_domain_user and win_domain_group
The onboarding consists of two plays:
- Read CSV in the Polycrate container (localhost, allowed because only files are read)
- 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_csvruns onlocalhostin the Polycrate container. It does not modify target systems, sohosts: localhostis 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_groupcreates all groups referenced in the CSV. - User Management:
community.windows.win_domain_usercreates 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.windowsis 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 toaudit.logunder{{ 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.csvmay have changed (e.g. new users).audit.logmay 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.logadds 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.polyand encrypted artifacts (AD credentials, no plaintext passwords ininventory.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_namedefault_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: