TL;DR
- Polycrate structures Ansible automation into three building blocks: Blocks, Actions, and Workspaces – eliminating the classic playbook sprawl and making automation discoverable and shareable.
- Blocks come with their own configuration, playbooks, templates, and artifacts. Workspace configuration overrides block defaults via deep-merge – ideal for reusable standards plus project-specific customizations.
- All blocks run in containers: No local Ansible setup, no Python version turbulence, and a unified toolchain for the entire team. Distribution is via an OCI registry and the PolyHub.
- Compared to Ansible Roles, Polycrate Blocks are self-contained, versionable, shareable via registries, and provide a clear execution and configuration framework – including workflows for more complex processes.
- ayedo develops Polycrate and provides a practical platform with official block collections in the PolyHub and documentation on Polycrate Blocks that scales from individual admins to large platform teams.
Block, Action, Workspace: The Core Principle
Polycrate builds on Ansible but gives automation a clearer form. Three concepts are central:
- Block: A self-contained module with
block.poly, playbooks, templates, and artifacts. - Action: A named entry point into a block (e.g.,
configure,patch,deploy) that executes exactly one playbook. - Workspace: The project context in which blocks are instantiated, configured, combined, and orchestrated in workflows.
Important: Polycrate always runs Ansible in a container. This solves the classic dependency problem:
- No local Ansible installation
- No Python version conflicts
- No heterogeneous tool setups in the team
Instead of complex Ansible CLI calls like:
ansible-playbook -i inventory.yml site.yml -e "env=prod" --tags "patch"
you start a defined action:
polycrate run linux-baseline patch
The action knows its playbook, configuration, and container context. This is good DX/UX – even colleagues without deep Ansible knowledge can safely execute automations.
Blocks are described in detail in the official documentation on Polycrate Blocks.
Block Anatomy in Detail
Let's look at a specific block that deploys Linux baseline configuration and patches. Directory structure in the workspace:
acme-corp-automation/
workspace.poly
inventory.yml
blocks/
linux-baseline/
block.poly
patch.yml
harden.yml
templates/
motd.j2
artifacts/
baseline-report.md
block.poly: Definition, Configuration, and Actions
# blocks/linux-baseline/block.poly
name: linux-baseline
version: 0.1.0
kind: generic
config:
os:
family: linux
reboot_after_patch: true
packages:
common:
- vim
- curl
- htop
motd:
enabled: true
text: "Managed by Polycrate – ACME Corp"
actions:
- name: patch
playbook: patch.yml
description: "Installs updates and optionally reboots"
- name: harden
playbook: harden.yml
description: "Applies basic hardening (SSH, Sysctl, MOTD)"
Key points:
config: Default values of the block. These are later overridden or supplemented in the workspace.actions: Each action refers to exactly one playbook in the same directory.version: Version of the block template – important once the block is shared via a registry.
Inventory in the Workspace
The inventory is always in the workspace root as a YAML file inventory.yml. Polycrate automatically sets ANSIBLE_INVENTORY.
# inventory.yml
all:
hosts:
server01.acme-corp.com:
ansible_user: ubuntu
server02.acme-corp.com:
ansible_user: ubuntu
The inventory is YAML in the workspace root (not a classic INI-style hosts file).
Ansible Playbook: Using block.config.*
The patch.yml accesses the block configuration:
# blocks/linux-baseline/patch.yml
- name: Patch Linux with Polycrate
hosts: all
become: true
gather_facts: true
vars:
reboot_after_patch: "{{ block.config.os.reboot_after_patch }}"
tasks:
- name: Update APT Cache
ansible.builtin.apt:
update_cache: true
when: ansible_facts.os_family == "Debian"
- name: Upgrade all packages (Debian/Ubuntu)
ansible.builtin.apt:
upgrade: dist
autoremove: true
when: ansible_facts.os_family == "Debian"
- name: Install YUM/DNF updates (RHEL/CentOS)
ansible.builtin.yum:
name: "*"
state: latest
when: ansible_facts.os_family in ["RedHat", "Rocky", "AlmaLinux", "CentOS"]
- name: Optional reboot after patches
ansible.builtin.reboot:
msg: "Reboot triggered by Polycrate linux-baseline.patch"
reboot_timeout: 600
when: reboot_after_patch | bool
The magic happens at block.config.os.reboot_after_patch: This variable does not come from vars_files or -e, but directly from block.poly or workspace configuration.
Configuration Inheritance and Deep-Merge
For a block to be truly reusable, it needs meaningful defaults – but also the ability to be overridden project-specifically. This is where Polycrate's config inheritance (deep-merge) comes in.
Block Defaults in block.poly
As seen above:
config:
os:
family: linux
reboot_after_patch: true
packages:
common:
- vim
- curl
- htop
motd:
enabled: true
text: "Managed by Polycrate – ACME Corp"
Workspace Instance with Overrides
In the workspace, define how the block should look specifically for ACME Corp:
# workspace.poly
name: acme-corp-automation
organization: acme
blocks:
- name: linux-baseline
from: cargo.ayedo.cloud/ayedo/infra/linux-baseline:0.3.1
config:
os:
reboot_after_patch: false
packages:
common:
- vim
- curl
- htop
- jq
motd:
text: "ACME Corp – Managed Linux Server"
Important:
from:is the OCI registry reference including the version tag (:0.3.1). Once the block is available locally (on the firstpolycrate run …you can confirm automatic installation from the registry;polycrate blocks pullremains optional), it lives underblocks/cargo.ayedo.cloud/ayedo/infra/linux-baseline/(the path underblocks/mirrors the URL).- Under
config:, you override and extend the block defaults.
Deep-Merge Principle
Polycrate deeply merges block config and workspace config. This means:
- Maps are recursively merged.
- Scalar values are overwritten.
- Lists are replaced by default, not appended (best practices see Best Practices for Blocks).
For our example, the effective configuration at runtime is:
# Effective block.config.* in the action linux-baseline.patch
os:
family: linux # from block.poly
reboot_after_patch: false # overridden in workspace
packages:
common: # list completely replaced
- vim
- curl
- htop
- jq
motd:
enabled: true # from block.poly
text: "ACME Corp – Managed Linux Server" # overridden in workspace
This achieves:
- A reusable standard block (e.g., maintained by a platform team)
- Fine tenant/project-specific customizations in the workspaces
- Clear separation between block definition and usage
More details on inheritance can be found in the documentation on Inheritance in Polycrate.
Polycrate Variables in the Ansible Playbook
Polycrate provides a range of variables in every action without you having to explicitly pass them:
block.config.*– the merged block configuration (as shown above)workspace.*– information and configuration of your workspaceaction.*– metadata about the currently running action
An extended example:
# blocks/linux-baseline/harden.yml
- name: Linux Baseline Hardening with Polycrate
hosts: all
become: true
gather_facts: false
tasks:
- name: Disable SSH Root Login
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
create: false
backup: true
- name: Set MOTD if enabled
ansible.builtin.copy:
content: "{{ block.config.motd.text }}"
dest: /etc/motd
when: block.config.motd.enabled | bool
- name: Log Workspace Name
ansible.builtin.lineinfile:
path: /var/log/polycrate.log
line: "Hardened by workspace {{ workspace.name }} via action {{ action.name }}"
create: yes
Here, all three levels come together:
block.config.motd.*– block or workspace-specific configurationworkspace.name– e.g.,acme-corp-automationaction.name– e.g.,harden
When working with secrets (e.g., SSH keys, certificates), these typically reside under artifacts/secrets/ in the workspace and are protected by the integrated Workspace Encryption with age. In the playbook, they are available via workspace.secrets['filename'].
From Action to Command: Execution with polycrate run
You can start the harden action above simply by:
cd acme-corp-automation
polycrate run linux-baseline harden
Polycrate ensures:
- A container with Ansible and the defined toolchain is started
- The workspace is mounted into the container
- Environment variables (e.g.,
ANSIBLE_INVENTORY) are set - Variables
block,workspace,actionare provided in the play - The action's playbook (e.g.
harden.ymlin the block directory, typicallyblocks/cargo.ayedo.cloud/ayedo/infra/linux-baseline/once the block is available locally) is executed against the defined inventory
With plain Ansible, you would need to:
- Provide Ansible and dependencies locally
- Manually manage
ansible.cfgand inventory paths - Assemble and pass extra variables yourself
With Polycrate, this context is modeled in block and workspace.
More on Ansible integration is described in the official Ansible Integration of Polycrate.
Versioning and Distribution via OCI Registry
You use the same registry reference (cargo.ayedo.cloud/...) to share and version blocks—in your team or via PolyHub.
Polycrate uses OCI registries—the same protocol as Docker/OCI container images. The from line in the workspace is the canonical source; the version is always a tag at the end (:0.3.1).
Key points:
- Never use
:latest—you want reproducible automation and controlled updates. - For local development (before push), you create the block under
blocks/<name>/—once published, you still reference it inworkspace.polywith the full registry URL (see Inheritance).
Pushing and Pulling Blocks from the Registry
As a block author, you want to share your work:
# In the block directory
cd acme-corp-automation/blocks/linux-baseline
# Push to the registry (version is in block.poly)
polycrate blocks push cargo.ayedo.cloud/acme/infra/linux-baseline
On another system or in another workspace, you can then use the same block:
# Make block available locally from registry (optional – often the first polycrate run is enough)
polycrate blocks pull cargo.ayedo.cloud/acme/infra/linux-baseline:0.1.0
You often do not need an explicit polycrate blocks pull: if the block is missing locally, Polycrate detects that on the first polycrate run … and asks whether to install the block automatically. Then reference it in workspace.poly with this URL. This is shareable automation in its purest form: built once, usable many times – in your team.