TL;DR
- PolyHub functions like an app store for infrastructure: Ready-made ayedo blocks for Kubernetes apps (nginx, cert-manager, external-dns, and many more) can be pulled directly from the registry
cargo.ayedo.cloudinto your workspace and used without a local Ansible setup. - In this post, you'll build a complete ingress stack (nginx + cert-manager + external-dns) solely by configuring your
workspace.polyand executing a fewpolycratecommands – including clean Kubeconfig handling and versioned blocks. - You'll learn how an official ayedo Kubernetes block is structured (block.poly + Ansible playbook), why version pinning (
:0.2.2instead of:latest) is mandatory in production, and how to leverage the extensive ecosystem on PolyHub for your own workspaces. - Polycrate solves the classic Ansible dependency problem by running all playbooks in a predefined container with a complete toolchain (kubectl, Helm, Python, Collections) – identical on every workstation, without Python chaos and without manual setup.
- ayedo provides a tested, reusable foundation for Kubernetes solutions with Polycrate and the official blocks on PolyHub, which you can directly adopt and adapt to your environment.
PolyHub as an App Store for Kubernetes Infrastructure
If you've ever set up an ingress stack manually with plain Ansible or Helm, you know the pattern:
- Gather values and CRDs for ingress-nginx
- Configure cert-manager with the correct issuers
- Connect external-dns to your DNS provider
- Version and document everything cleanly
With Polycrate and PolyHub, you reverse the principle: Instead of modeling everything yourself, you start with ready-made, tested building blocks.
PolyHub at https://hub.polycrate.io is a registry of Polycrate blocks, stored as OCI images in cargo.ayedo.cloud – similar to container images on Docker Hub, but specifically for automation. For Kubernetes, you'll find among others:
cargo.ayedo.cloud/ayedo/k8s/nginx:*– Ingress Controllercargo.ayedo.cloud/ayedo/k8s/cert-manager:*– TLS Automationcargo.ayedo.cloud/ayedo/k8s/external-dns:*– DNS Automation
The core ideas behind this:
- Sharable Automation: Once built as a block, it can be reused in any workspace and shared via an OCI registry.
- Guardrails instead of Overgrowth: Instead of loosely scattered playbooks, you encapsulate logic in clearly defined blocks with a configuration interface.
- Containerized Execution: All Ansible playbooks run in a Polycrate container – no local Ansible installation, no Python version conflicts, no manually installed collections.
In this post, we use exactly these building blocks to build a production-ready ingress stack for an example cluster.
Preparing the Workspace: Declare Kubeconfig and Blocks
We start with a workspace acme-corp-automation. It is the logical unit for your automation – whether you're managing Linux servers, Windows hosts, or Kubernetes clusters.
A minimal workspace.poly could start like this:
name: acme-corp-automation
organization: acme
config: {}
blocks: []
workflows: []
The top-level config field is for workspace/toolchain settings (for example the container image under config.image) – not a free-form bag for arbitrary automation variables; those belong in block config or secrets.poly. See Configuration. The empty config: {} here is only a placeholder until you set an image, for example.
Important for Kubernetes: The Kubeconfig of your cluster is always stored as a secret file in the workspace in Polycrate:
artifacts/secrets/kubeconfig.yml
This file is treated as a secret by Polycrate and can be protected with the built-in workspace encryption, see Workspace Encryption.
To activate encryption, simply run on the CLI:
polycrate workspace encrypt
You don't need an external vault – Polycrate uses age internally and integrates it into your workflow execution, which often significantly simplifies compliance requirements.
Pulling a Kubernetes Block from PolyHub
The official ayedo blocks for Kubernetes apps are available as OCI images on cargo.ayedo.cloud. You can pull any block into your workspace:
polycrate blocks pull cargo.ayedo.cloud/ayedo/k8s/nginx:0.2.2
Here's what happens:
- Polycrate loads the
nginxblock in version0.2.2from the registry. - The block is stored in the workspace cache; you can reference it from there.
- No local Ansible,
kubectl, or Helm is necessary – all of this comes from the Polycrate container.
Important: The version specification :0.2.2 is not a detail – it is a best practice. In production, you should never use :latest, see also the Best Practices in the Documentation.
Now we integrate the block into our workspace.
Integrating and Configuring the Block in workspace.poly
In workspace.poly, you declare which block instances you want to use. For our ingress stack, we add three blocks:
name: acme-corp-automation
organization: acme
blocks:
- name: k8s-nginx
from: cargo.ayedo.cloud/ayedo/k8s/nginx:0.2.2
config:
namespace: "ingress-nginx"
ingress_class: "nginx"
- name: k8s-cert-manager
from: cargo.ayedo.cloud/ayedo/k8s/cert-manager:0.3.0
config:
namespace: "cert-manager"
email: "admin@acme-corp.com"
- name: k8s-external-dns
from: cargo.ayedo.cloud/ayedo/k8s/external-dns:0.4.1
config:
namespace: "external-dns"
provider: "route53"
domain_filter: "acme-corp.com"
workflows: []
A few important points:
from:always references the full block name including version (:0.2.2).name:is the instance name in the workspace – this is how you refer to the block later (polycrate run k8s-nginx deploy).config:contains block-specific values. These later appear asblock.config.*in your Ansible playbook.
The beauty: You don't need to know the internal logic of these blocks to use them. They are like meaningful building blocks in a kit – neatly encapsulated and versioned.
How an ayedo Kubernetes Block is Structured
Let's take a look at how such an ayedo block looks inside. Imagine you were developing cargo.ayedo.cloud/ayedo/k8s/nginx:0.2.2 locally. In the block directory, there would be a block.poly:
name: k8s-nginx
version: 0.2.2
kind: generic
config:
namespace: "ingress-nginx"
ingress_class: "nginx"
actions:
- name: deploy
playbook: deploy.yml
- name: delete
playbook: delete.yml
Important details:
- Kubeconfig: You do not need a
kubeconfig_pathinblock.poly. Polycrate takes the kubeconfig fromartifacts/secrets/kubeconfig.ymland setsKUBECONFIGin the action container to the correct path –kubectl, Helm, and Ansible modules (for examplecommunity.kubernetes.helm) use it without extra block configuration..polyfiles also do not support Jinja like{{ workspace.secrets[...] }}. actionsdefine which commands you can execute viapolycrate run(deploy,delete, ...).- The version
0.2.2is hard-coded in theblock.polyand is also used in the registry.
This includes an Ansible playbook, e.g., deploy.yml:
- name: Deploy ingress-nginx via Helm
hosts: localhost
gather_facts: false
tasks:
- name: Install ingress-nginx Helm chart
community.kubernetes.helm:
name: ingress-nginx
chart_ref: ingress-nginx/ingress-nginx
release_namespace: "{{ block.config.namespace }}"
create_namespace: true
values:
controller:
ingressClassResource:
name: "{{ block.config.ingress_class }}"
A few explanations:
hosts: localhostis correct here because the playbook only interacts with the Kubernetes API. It does not alter local packages – it runs in the Polycrate container and controls the cluster from there.- The module
community.kubernetes.helmcomes from an Ansible collection that is already installed in the Polycrate container. You don't have to worry about this dependency.KUBECONFIGis already set in the container – an explicitkubeconfig:task parameter is not required. block.config.*are the values from yourworkspace.polyconfiguration for this block instance (here, among others,namespace,ingress_class). You control the deployment entirely from the workspace.
With plain Ansible, you would need to:
- Set up a Python environment with matching versions
- Install
kubectl,helm, Ansible plus collections - Wire up kubeconfig access yourself (Polycrate does this via
KUBECONFIG) - Organize your playbooks and roles in a free directory tree
With Polycrate, you get a clearly structured block definition, a standardized execution environment in the container, and a consistent way to pass configuration from the workspace into the playbooks. See also the Ansible Integration in the Documentation.
Executing the Complete Ingress Stack as a Workflow
A major advantage of the block structure is that you can build a reproducible workflow from multiple blocks. In our example:
k8s-nginx– Ingress Controllerk8s-cert-manager– TLS Certificatesk8s-external-dns– DNS Entries for your Ingresses
We define this stack as a workflow in workspace.poly:
name: acme-corp-automation
organization: acme
blocks:
- name: k8s-nginx
from: cargo.ayedo.cloud/ayedo/k8s/nginx:0.2.2
config:
namespace: "ingress-nginx"
ingress_class: "nginx"
- name: k8s-cert-manager
from: cargo.ayedo.cloud/ayedo/k8s/cert-manager:0.3.0
config:
namespace: "cert-manager"
email: "admin@acme-corp.com"
- name: k8s-external-dns
from: cargo.ayedo.cloud/ayedo/k8s/external-dns:0.4.1
config:
namespace: "external-dns"
provider: "route53"
domain_filter: "acme-corp.com"
workflows:
- name: k8s-ingress-stack
actions:
- block: k8s-nginx
action: deploy
- block: k8s-cert-manager
action: deploy
- block: k8s-external-dns
action: deploy
With this, you can roll out the complete stack with:
polycrate workflows run k8s-ingress-stack
Here's what happens:
- Polycrate starts a container for each action, where Ansible is executed with the respective block configuration.
- Your Kubeconfig is piped from
artifacts/secrets/kubeconfig.ymlinto the container, without needing to be unencrypted in the repository. - Each block uses its own tested logic, but all follow the same UX guidelines (uniform actions like
deploy,delete,upgrade).
With plain Ansible, you would typically:
- Set up a folder structure like
playbooks/k8s/nginx.yml,playbooks/k8s/cert-manager.yml,playbooks/k8s/external-dns.yml - Remember (or document) the order in which you need to call these playbooks
- Assume a working Ansible environment on every developer laptop
With Polycrate, these playbooks are packaged into reusable building blocks and orchestrated via workflows. This prevents the playbook overgrowth that many teams know from evolving environments and is detailed in the Best Practices.
Version Pinning: Why :latest is Taboo in Production
A central principle in productive Kubernetes solutions is reproducibility. That's exactly why :latest is not a good idea for container images – and the same goes for Polycrate blocks.
The registry URL of a block always contains the version:
cargo.ayedo.cloud/ayedo/k8s/nginx:0.2.2
Best practices include:
- Always pin explicitly: Always use a specific version (
:0.2.2) inworkspace.poly. - Upgrade consciously: