Polycrate API for Teams: Centralized Monitoring and Remote Triggering
TL;DR The Polycrate API transforms individual workspaces into a team platform: all workspaces, …
Diese Serie zeigt Schritt für Schritt, wie Ansible mit Polycrate zu einer strukturierten, teilbaren und compliance-fähigen Automatisierungsplattform wird – von den Grundlagen bis zu Enterprise-Szenarien.
community.general.certbot) on a Linux server – including a generic nginx.conf as a Jinja2 template.block.config.* and can be used in any workspace with different values – without code duplication or wiki tinkering.from: cargo.ayedo.cloud/ayedo/infra/nginx-letsencrypt:1.0.0 in workspace.poly), you share the block with colleagues: before pushing, name in block.poly must be the full OCI reference without a tag; polycrate blocks push uses that name plus version as the tag – sharing instead of screenshots.Many admins know the scenario: An Nginx has been running on web01.acme-corp.com for years, the nginx.conf was “quickly” adjusted back then, Let’s Encrypt was set up somewhere with certbot – and no one remembers exactly how.
Documentation, if any, is in a wiki entry that no longer matches reality. Automation consists of individual shell snippets that were only ever tried on this server.
With plain Ansible, this can be done better, but you know the hurdles:
In this post, I’ll show you how to turn exactly this setup into a reusable Polycrate block that your colleagues can easily use – without needing to know the implementation.
Polycrate builds directly on Ansible but solves two typical problems:
Dependency Problem:
Ansible runs in a container. No local installation, no Python zoo, no different version states in the team. The block brings everything it needs.
Sharable Automation:
Your Nginx setup is no longer a fragile wiki article but a versioned block that resides in a registry. What you build, others can use immediately – with their own domains and ports.
We focus on a specific block:
nginx.conf based on a Jinja2 templateblock.config.domainblock.config.emailblock.config.upstream_portStarting point is an existing Polycrate workspace, e.g.:
# workspace.poly
name: acme-corp-automation
organization: acmeOur block is located in ./blocks/nginx-letsencrypt/ and contains:
block.polynginx-letsencrypt.yml (Ansible playbook)nginx.conf.j2 (Jinja2 template)How the workspace root and block directory relate (example workspace name acme-corp-automation):
acme-corp-automation/ # Workspace root
├── workspace.poly # Workspace config, including block instances
├── inventory.yml # Central Ansible inventory (all blocks)
├── blocks/
│ └── nginx-letsencrypt/ # Local block (directory name = template name in from:)
│ ├── block.poly # Block definition: version, config, actions
│ ├── nginx-letsencrypt.yml # Ansible playbook (deploy action)
│ └── nginx.conf.j2 # Jinja2 template for Nginx
└── … # other typical entries (e.g. artifacts/, secrets.poly)Later, when you publish the block to a registry with a full OCI name, the same files usually live under a path that mirrors the registry name, e.g. blocks/registry.acme-corp.com/infra/nginx/ (see Pushing to a registry).
name: nginx-letsencrypt
version: 1.0.0
kind: generic
config:
domain: ""
email: ""
upstream_port: 8080
actions:
- name: deploy
playbook: nginx-letsencrypt.ymlKey points:
version: 1.0.0 – we explicitly version the block.config defines the parameters to be set later in the workspace:
domain – FQDN like www.acme-corp.comemail – contact email for Let’s Encryptupstream_port – port of the backend that Nginx proxies to (e.g., an app on 8080)actions describes which playbooks are available.polycrate run nginx-letsencrypt deploy, we execute the playbook.Further details on block structure can be found in the official Block Concepts.
The core for reusability is the Nginx configuration as a template. Instead of fixed domain names and ports, we use Polycrate variables.
nginx.conf.j2# nginx.conf for {{ block.config.domain }} – generated by Polycrate
server {
listen 80;
server_name {{ block.config.domain }};
# HTTP → HTTPS Redirect
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ block.config.domain }};
ssl_certificate /etc/letsencrypt/live/{{ block.config.domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ block.config.domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Optional HSTS – adjust if needed
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:{{ block.config.upstream_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}What happens here:
{{ block.config.domain }} and {{ block.config.upstream_port }} come directly from block.config in the block – and are ultimately set in the workspace./etc/letsencrypt/live/...).Now we connect everything in a playbook. Important note in Polycrate: The playbooks run in the container, but the target hosts are real servers from the inventory, not localhost.
In the workspace root is inventory.yml (Polycrate sets ANSIBLE_INVENTORY automatically):
# inventory.yml
all:
hosts:
web01.acme-corp.com:
ansible_user: ubuntu
ansible_become: truenginx-letsencrypt.yml---
- name: Deploy Nginx with Let's Encrypt
hosts: all
become: true
vars:
domain_name: "{{ block.config.domain }}"
contact_email: "{{ block.config.email }}"
upstream_port: "{{ block.config.upstream_port }}"
pre_tasks:
- name: Domain must be set
ansible.builtin.assert:
that:
- domain_name | length > 0
- contact_email | length > 0
fail_msg: "block.config.domain and block.config.email must be set in the workspace."
tasks:
- name: Update APT cache (Ubuntu/Debian)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install Nginx and Certbot
ansible.builtin.apt:
name:
- nginx
- certbot
- python3-certbot
state: present
- name: Disable default site
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: Obtain TLS certificate from Let's Encrypt (Standalone)
community.general.certbot:
email: "{{ contact_email }}"
domains:
- "{{ domain_name }}"
authenticator: standalone
agree_tos: true
installer: none
state: present
register: certbot_result
- name: Show Certbot output (Debug)
ansible.builtin.debug:
var: certbot_result
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: "/etc/nginx/sites-available/{{ domain_name }}"
owner: root
group: root
mode: "0644"
notify: reload nginx
- name: Enable site
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ domain_name }}"
dest: "/etc/nginx/sites-enabled/{{ domain_name }}"
state: link
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
enabled: trueExplanations:
hosts: all – all hosts from inventory.yml. No connection: local, because we manage real servers.community.general.certbot comes from the community.general collection. Polycrate can install this in the container without touching your local system (see Ansible Integration).standalone authenticator:
vars, we bind the Polycrate variables (block.config.*) to Ansible variables, simplifying reading in the playbook.After building and polycrate blocks push, workspaces consume the block the same way as in the rest of this series: full OCI reference under cargo.ayedo.cloud (not only the short template name).
# workspace.poly
name: acme-corp-automation
organization: acme
blocks:
- name: webserver
from: cargo.ayedo.cloud/ayedo/infra/nginx-letsencrypt:1.0.0
config:
domain: "www.acme-corp.com"
email: "admin@acme-corp.com"
upstream_port: 8080name: webserver is the instance name in the workspace.cargo.ayedo.cloud. You do not need to run polycrate blocks pull first: if the block is missing locally, Polycrate detects that when you run polycrate run … and asks whether to install the block from the registry automatically. After that, the unpacked tree lives under blocks/cargo.ayedo.cloud/ayedo/infra/nginx-letsencrypt/. In from: use the full reference including the tag (block sharing via OCI, not only the local blocks/nginx-letsencrypt/ folder).config, we override the default values from block.poly.polycrate run webserver deployPolycrate:
inventory.yml from the workspace root.block.config.* and executes the playbook in the container.With plain Ansible, it would look like this:
ansible-playbook -i inventory.yml nginx-letsencrypt.yml \
-e "domain_name=www.acme-corp.com contact_email=admin@acme-corp.com upstream_port=8080"And you would have to ensure that:
community.general collection is availableWith Polycrate, these dependencies are part of the block or workspace and run in the container.
Now comes the sharing argument: Instead of writing a wiki entry, you publish your block in a registry.
Important: Blocks with purely local names like nginx-letsencrypt or nginx cannot be pushed to a registry. polycrate blocks push expects name in block.poly to already be the full OCI reference without a version tag (host and path like a container image name without :tag). The version lives in the separate version: field and becomes the OCI tag on push.
Assuming you operate an internal Polycrate/OCI registry at registry.acme-corp.com.
block.poly for the registryBefore pushing, set the registry name and version as usual:
name: registry.acme-corp.com/infra/nginx
version: 1.0.0
kind: generic
config:
domain: ""
email: ""
upstream_port: 8080
actions:
- name: deploy
playbook: nginx-letsencrypt.ymlRecommended: mirror the name in the directory layout, e.g. blocks/registry.acme-corp.com/infra/nginx/ (see Best practices).
polycrate blocks push registry.acme-corp.com/infra/nginxThere is only this one argument – the block name exactly as in block.poly (name, without tag). There is no second argument for a “destination URL”; Polycrate derives host, path, and tag from name and version.
What happens:
block.poly, playbook, template) as an OCI artifact.registry.acme-corp.com/infra/nginx:1.0.0 (name + version as tag).version in block.poly (e.g. 1.1.0) and push again.When others consume the block, they reference it with from: including a tag – best practice is to pin the block version when declaring the instance in the workspace, e.g. from: registry.acme-corp.com/infra/nginx:1.0.0 (see Registry documentation).
Your colleague has a different workspace, e.g., for an internal portal. They want to use the same Nginx/Let’s Encrypt block, but with different values.
# workspace.poly in the colleague's workspace
name: acme-portal-automation
organization: acme
blocks:
- name: portal-web
from: registry.acme-corp.com/infra/nginx:1.0.0
config:
domain: "portal.acme-corp.com"
email: "ops@acme-corp.com"
upstream_port: 9000They don’t need to:
They just set the configuration.
TL;DR The Polycrate API transforms individual workspaces into a team platform: all workspaces, …
TL;DR Polycrate not only logs Action Runs (Ansible playbooks) but also SSH sessions, workspace …
TL;DR The Model Context Protocol (MCP) is an open standard: AI clients talk to helper programs over …