Ansible
Polycrate's Ansible integration: snapshot variables in playbooks and templates, the workspace inventory, Ansible output configuration and the ansible-polycrate callback, Docker and Kubernetes examples, advanced playbooks and best practices.
Ansible
Polycrate offers a special integration with Ansible. The workspace snapshot, which is exported in YAML format and added to the Polycrate container, is processed automatically by Ansible. As a result, the snapshot is directly available as top-level variables in Ansible and can be used in playbooks and templates.
The following example shows:
- the default configuration (block.poly) of a block named
traefik - the configuration provided by the user for the block in
workspace.poly - the Ansible playbook that uses the exposed variables (block.config...)
- an Ansible template that uses the exposed variables (
templates/docker-compose.yml.j2) - the resulting file
/polycrate/docker-compose.ymlthat is transferred to a remote host
The block variable contains the configuration of the current block, which is invoked by
polycrate run traefik install
In addition, there is a variable named workspace that contains the fully compiled workspace including additional blocks available in the workspace.
Polycrate uses a special Ansible vars plugin to read the YAML snapshot and make it available as a top-level variable in the Ansible facts.
# block.poly
name: traefik
config:
image: "traefik:v2.6"
letsencrypt:
email: ""
resolver: letsencrypt
actions:
- name: install
playbook: install.yml
- name: uninstall
playbook: uninstall.yml
- name: prune
playbook: prune.yml
# workspace.poly
name: ansible-traefik-demo
blocks:
- name: traefik
config:
letsencrypt:
email: info@example.com
image: traefik:2.7
# install.yml
- name: "install"
hosts: all
gather_facts: yes
tasks:
- name: Create remote block directory
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
with_items:
- "/polycrate/{{ block.name }}"
- name: Copy compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "/polycrate/{{ block.name }}/docker-compose.yml"
- name: Deploy compose stack
docker_compose:
project_src: "/polycrate/{{ block.name }}"
remove_orphans: true
files:
- docker-compose.yml
# templates/docker-compose.yml.j2
version: "3.9"
services:
traefik:
image: "{{ block.config.image }}"
container_name: "traefik"
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.{{ block.config.letsencrypt.resolver }}.acme.email={{ block.config.letsencrypt.email }}"
- "--certificatesresolvers.{{ block.config.letsencrypt.resolver }}.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.{{ block.config.letsencrypt.resolver }}.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "traefik-letsencrypt:/letsencrypt"
networks:
- traefik
networks:
traefik:
name: traefik
volumes:
traefik-letsencrypt:
# /polycrate/traefik/docker-compose.yml
version: "3.9"
services:
traefik:
image: "traefik:2.7"
container_name: "traefik"
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=info@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "traefik-letsencrypt:/letsencrypt"
networks:
- traefik
networks:
traefik:
name: traefik
volumes:
traefik-letsencrypt:
Ansible inventory
The Ansible inventory is stored as inventory.yml directly in the workspace root. Polycrate sets ANSIBLE_INVENTORY to this path automatically, so Ansible knows which hosts to address without further configuration.
The inventory must be in YAML format (no INI):
# inventory.yml (in the workspace root)
all:
hosts:
host-1:
ansible_host: 1.2.3.4
ansible_ssh_port: 22
ansible_python_interpreter: "/usr/bin/python3"
ansible_user: root
host-2:
ansible_host: 1.2.3.5
ansible_user: root
children:
webservers:
hosts:
host-1:
host-2:
The corresponding workspace.poly does not need an inventory block – the inventory from the workspace root is used automatically:
# workspace.poly
name: acme-corp-automation
blocks:
- name: my-block
config:
foo: bar
Playbooks address the hosts via their group:
# my-playbook.yml
- name: Configure webservers
hosts: webservers
gather_facts: true
become: true
tasks:
- name: Ensure nginx is installed
ansible.builtin.apt:
name: nginx
state: present
Ansible output configuration
Polycrate controls the Ansible output independently of its own CLI output format. The settings can be configured persistently via ~/.polycrate/polycrate.yml and overridden per run with CLI flags.
ansible-polycrate callback (default since CLI 0.33.0)
Since CLI 0.33.0 the Polycrate container image ships its own Ansible callback plugin ansible-polycrate, which is active by default. The callback summarizes the task output considerably more compactly than the stock default callback:
- One line per host and task (status, optionally
changeddiff) - Redundant task metadata is suppressed
- Failed tasks are shown with full context including traceback
The plugin is automatically registered as stdout_callback in the ansible.cfg of the Polycrate container – no configuration required. If you need the behavior of the stock callbacks (e.g. for external consumers of the output), you can override the callback at any time via CLI flag (--ansible-stdout-callback ansible.builtin.default) or via ~/.polycrate/polycrate.yml.
Defaults
| Setting | Default | Description |
|---|---|---|
stdout_callback |
ansible-polycrate |
Callback plugin that formats the output (fallback: ansible.builtin.default) |
callback_result_format |
json |
Result format for the stock callback: json (compact) or yaml (verbose) |
display_ok_hosts |
true |
Shows hosts with status ok |
display_skipped_hosts |
false |
Shows skipped hosts |
The json format prints a single line per host — much more compact than the earlier yaml format, which produced 3–10 lines per host for each task.
Compact JSON output (default):
changed: [node1] => {"msg": "nginx installed", "changed": true}
changed: [node2] => {"msg": "nginx installed", "changed": true}
ok: [node3] => {"msg": "already installed", "changed": false}
Verbose YAML output (optional):
changed: [node1] =>
msg: nginx installed
changed: true
changed: [node2] =>
msg: nginx installed
changed: true
Configuration via polycrate.yml
Persistent settings are stored in ~/.polycrate/polycrate.yml under the key ansible:
# ~/.polycrate/polycrate.yml
ansible:
stdout_callback: ansible.builtin.default # Callback plugin
callback_result_format: json # json | yaml
display_ok_hosts: true
display_skipped_hosts: false
Example: maximum compactness with the dense callback
The dense callback condenses all hosts of a task into a single line and is ideal for playbooks with many hosts:
ansible:
stdout_callback: ansible.builtin.dense
Example: verbose output for debugging
ansible:
callback_result_format: yaml
display_ok_hosts: true
display_skipped_hosts: true
CLI flags
The following flags override the polycrate.yml configuration for a single run:
| Flag | Description |
|---|---|
--ansible-stdout-callback <plugin> |
Sets the Ansible stdout callback |
--ansible-callback-result-format <format> |
Sets the result format (json or yaml) |
Available for polycrate run and polycrate actions run.
# Single run with full YAML output (e.g. for debugging)
polycrate run myblock myaction --ansible-callback-result-format yaml
# Maximum compactness for a single run
polycrate run myblock myaction --ansible-stdout-callback ansible.builtin.dense
# Unix-style output (requires the community.general collection)
polycrate run myblock myaction --ansible-stdout-callback community.general.unixy
Task progress in real time (stock callback)
When using the stock default callback, the task banner (TASK [name]) may only appear with the next task – especially in playbooks with many conditionals. The reason: the default plugin suppresses the banner as long as display_skipped_hosts=False (CLI 0.35.0 analysis).
Remedy for real-time banners:
ansible:
display_skipped_hosts: true
Or via environment variable:
ANSIBLE_DISPLAY_SKIPPED_HOSTS=True polycrate run myblock myaction
With the default callback ansible-polycrate (since CLI 0.33.0) the problem is not relevant – banners are printed in real time regardless of display_skipped_hosts.
Ansible for Docker
Ansible offers comprehensive modules for Docker and Docker Compose management. With Polycrate you can elegantly manage Docker containers and Docker Compose stacks via Ansible playbooks.
Deploying Docker Compose with Ansible
The Traefik example above already shows a typical Docker Compose workflow with Ansible. Here are further details:
Complete block with Docker Compose:
# block.poly
name: my-docker-stack
kind: dockerapp
config:
app:
image: myapp:latest
port: 8080
database:
image: postgres:14
user: appuser
password: "{{ vault_db_password }}"
name: appdb
actions:
- name: install
playbook: docker-compose.yml
- name: uninstall
playbook: docker-compose-down.yml
Playbook:
# docker-compose.yml
---
- name: Deploy Docker Compose Stack
hosts: all
become: yes
tasks:
- name: Create App Directory
file:
path: "/opt/{{ block.name }}"
state: directory
mode: '0755'
- name: Copy docker-compose.yml
template:
src: templates/docker-compose.yml.j2
dest: "/opt/{{ block.name }}/docker-compose.yml"
- name: Start Docker Compose Stack
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
state: present
pull: yes
remove_orphans: yes
Template:
# templates/docker-compose.yml.j2
version: '3.8'
services:
web:
image: {{ block.config.app.image }}
ports:
- "{{ block.config.app.port }}:80"
environment:
- DATABASE_URL=postgresql://{{ block.config.database.user }}:{{ block.config.database.password }}@db:5432/{{ block.config.database.name }}
depends_on:
- db
db:
image: {{ block.config.database.image }}
environment:
- POSTGRES_USER={{ block.config.database.user }}
- POSTGRES_PASSWORD={{ block.config.database.password }}
- POSTGRES_DB={{ block.config.database.name }}
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
Ansible for Kubernetes
Ansible offers powerful modules for Kubernetes management. With Polycrate you can elegantly manage Kubernetes resources via Ansible playbooks - whether with raw manifests, Helm charts or the Kubernetes module.
1. Kubernetes manifests with Ansible
Deploying a service with Kubernetes manifests:
# block.poly
name: my-k8s-app
kind: k8sapp
config:
namespace: production
app:
name: myapp
image: myapp:1.0.0
replicas: 3
port: 8080
actions:
- name: install
playbook: install.yml
- name: uninstall
playbook: uninstall.yml
Playbook:
# install.yml
---
- name: Deploy Application to Kubernetes
hosts: localhost
gather_facts: no
tasks:
- name: Create Namespace
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ block.config.namespace }}"
- name: Deploy Application from Template
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
template: "templates/deployment.yml.j2"
- name: Deploy Service from Template
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
template: "templates/service.yml.j2"
- name: Deploy Ingress from Template
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
template: "templates/ingress.yml.j2"
- name: Wait for Deployment to be ready
kubernetes.core.k8s_info:
kind: Deployment
namespace: "{{ block.config.namespace }}"
name: "{{ block.config.app.name }}"
register: deployment
until: deployment.resources[0].status.readyReplicas == block.config.app.replicas
retries: 30
delay: 10
Templates:
# templates/deployment.yml.j2
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ block.config.app.name }}
labels:
app: {{ block.config.app.name }}
spec:
replicas: {{ block.config.app.replicas }}
selector:
matchLabels:
app: {{ block.config.app.name }}
template:
metadata:
labels:
app: {{ block.config.app.name }}
spec:
containers:
- name: {{ block.config.app.name }}
image: {{ block.config.app.image }}
ports:
- containerPort: {{ block.config.app.port }}
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# templates/service.yml.j2
apiVersion: v1
kind: Service
metadata:
name: {{ block.config.app.name }}
spec:
selector:
app: {{ block.config.app.name }}
ports:
- protocol: TCP
port: 80
targetPort: {{ block.config.app.port }}
type: ClusterIP
# templates/ingress.yml.j2
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ block.config.app.name }}
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
rules:
- host: {{ block.config.app.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ block.config.app.name }}
port:
number: 80
tls:
- hosts:
- {{ block.config.app.domain }}
secretName: {{ block.config.app.name }}-tls
2. Helm charts with Ansible
Deployment via Helm:
# block.poly
name: postgres-helm
kind: k8sapp
config:
namespace: databases
chart:
name: postgresql
repo:
name: bitnami
url: https://charts.bitnami.com/bitnami
version: 12.12.0
app:
auth:
username: pguser
password: "{{ vault_pg_password }}"
database: mydb
primary:
persistence:
size: 50Gi
storageClass: fast-ssd
replication:
enabled: true
replicas: 2
actions:
- name: install
playbook: helm-install.yml
- name: uninstall
playbook: helm-uninstall.yml
Playbook:
# helm-install.yml
---
- name: Install Helm Chart
hosts: localhost
gather_facts: no
tasks:
- name: Add Helm Repository
kubernetes.core.helm_repository:
name: "{{ block.config.chart.repo.name }}"
repo_url: "{{ block.config.chart.repo.url }}"
- name: Create Namespace
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ block.config.namespace }}"
- name: Install Helm Chart
kubernetes.core.helm:
name: "{{ block.name }}"
chart_ref: "{{ block.config.chart.repo.name }}/{{ block.config.chart.name }}"
chart_version: "{{ block.config.chart.version }}"
release_namespace: "{{ block.config.namespace }}"
values:
auth:
username: "{{ block.config.app.auth.username }}"
password: "{{ block.config.app.auth.password }}"
database: "{{ block.config.app.auth.database }}"
primary:
persistence:
size: "{{ block.config.app.primary.persistence.size }}"
storageClass: "{{ block.config.app.primary.persistence.storageClass }}"
readReplicas:
replicaCount: "{{ block.config.app.replication.replicas }}"
wait: yes
wait_timeout: 10m
- name: Wait for PostgreSQL to be ready
kubernetes.core.k8s_info:
kind: StatefulSet
namespace: "{{ block.config.namespace }}"
name: "{{ block.name }}-postgresql"
register: statefulset
until: statefulset.resources[0].status.readyReplicas >= 1
retries: 30
delay: 10
3. Complex multi-resource deployments
PostgreSQL with backup and monitoring:
# block.poly
name: postgres-complete
kind: k8sapp
config:
namespace: databases
postgres:
version: "14"
storage: 100Gi
replicas: 3
backup:
enabled: true
schedule: "0 2 * * *"
retention: 7
monitoring:
enabled: true
prometheus:
enabled: true
actions:
- name: install
playbook: install-complete.yml
Playbook:
# install-complete.yml
---
- name: Deploy Complete PostgreSQL Stack
hosts: localhost
gather_facts: no
tasks:
- name: Create Namespace
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ block.config.namespace }}"
labels:
monitoring: "true"
- name: Deploy CloudNativePG Operator (if not exists)
kubernetes.core.k8s:
state: present
src: "https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.21/releases/cnpg-1.21.0.yaml"
- name: Deploy PostgreSQL Cluster
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
definition:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: "{{ block.name }}"
spec:
instances: "{{ block.config.postgres.replicas }}"
imageName: "ghcr.io/cloudnative-pg/postgresql:{{ block.config.postgres.version }}"
storage:
size: "{{ block.config.postgres.storage }}"
monitoring:
enabled: "{{ block.config.monitoring.enabled }}"
backup:
barmanObjectStore:
destinationPath: "s3://my-backup-bucket/{{ block.name }}"
s3Credentials:
accessKeyId:
name: backup-s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: backup-s3-creds
key: SECRET_ACCESS_KEY
retentionPolicy: "{{ block.config.backup.retention }}d"
- name: Create Scheduled Backup (if enabled)
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
definition:
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: "{{ block.name }}-backup"
spec:
schedule: "{{ block.config.backup.schedule }}"
backupOwnerReference: self
cluster:
name: "{{ block.name }}"
when: block.config.backup.enabled
- name: Deploy ServiceMonitor for Prometheus (if enabled)
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
definition:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: "{{ block.name }}-metrics"
spec:
selector:
matchLabels:
cnpg.io/cluster: "{{ block.name }}"
endpoints:
- port: metrics
when: block.config.monitoring.prometheus.enabled
- name: Wait for PostgreSQL Cluster to be ready
kubernetes.core.k8s_info:
api_version: postgresql.cnpg.io/v1
kind: Cluster
namespace: "{{ block.config.namespace }}"
name: "{{ block.name }}"
register: pg_cluster
until: pg_cluster.resources[0].status.phase == "Cluster in healthy state"
retries: 60
delay: 10
4. ConfigMaps and secrets management
# deploy-with-config.yml
---
- name: Deploy Application with Config
hosts: localhost
gather_facts: no
tasks:
- name: Create ConfigMap from Template
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ block.config.app.name }}-config"
data:
app.conf: |
{{ lookup('template', 'templates/app.conf.j2') }}
database.url: "postgresql://{{ block.config.database.host }}:5432/{{ block.config.database.name }}"
- name: Create Secret from Vault
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ block.config.app.name }}-secrets"
type: Opaque
stringData:
db-password: "{{ vault_db_password }}"
api-key: "{{ vault_api_key }}"
- name: Deploy Application with Config
kubernetes.core.k8s:
state: present
namespace: "{{ block.config.namespace }}"
template: "templates/deployment-with-config.yml.j2"
# templates/deployment-with-config.yml.j2
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ block.config.app.name }}
spec:
replicas: {{ block.config.app.replicas }}
selector:
matchLabels:
app: {{ block.config.app.name }}
template:
metadata:
labels:
app: {{ block.config.app.name }}
spec:
containers:
- name: {{ block.config.app.name }}
image: {{ block.config.app.image }}
envFrom:
- configMapRef:
name: {{ block.config.app.name }}-config
- secretRef:
name: {{ block.config.app.name }}-secrets
volumeMounts:
- name: config
mountPath: /etc/config
volumes:
- name: config
configMap:
name: {{ block.config.app.name }}-config
5. Multi-cluster deployments
With Polycrate you can deploy to several Kubernetes clusters with different kubeconfigs:
# workspace.poly
name: multi-cluster-workspace
blocks:
# Cluster 1: Production EU
- name: prod-eu-cluster
kind: k8scluster
# Generates kubeconfig to artifacts/blocks/prod-eu-cluster/kubeconfig.yml
# Cluster 2: Production US
- name: prod-us-cluster
kind: k8scluster
# Generates kubeconfig to artifacts/blocks/prod-us-cluster/kubeconfig.yml
# App deployment to the EU cluster
- name: app-eu
from: my-app-base
kubeconfig:
from: prod-eu-cluster
config:
namespace: production
region: eu
# App deployment to the US cluster
- name: app-us
from: my-app-base
kubeconfig:
from: prod-us-cluster
config:
namespace: production
region: us
workflows:
- name: deploy-global
steps:
- name: deploy-eu
block: app-eu
action: deploy
- name: deploy-us
block: app-us
action: deploy
Ansible automatically uses the correct kubeconfig for each block.
Advanced Ansible examples
Ansible Vault for secrets
# Encrypt secrets
ansible-vault encrypt_string 'secret-password' --name 'vault_db_password'
# Reference in workspace.poly
polycrate run my-app install --ask-vault-pass
# block.poly with Vault variables
config:
database:
password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
Dynamic inventories
# create-inventory.yml
---
- name: Create Dynamic Inventory from Cloud Provider
hosts: localhost
gather_facts: no
tasks:
- name: Get EC2 Instances
amazon.aws.ec2_instance_info:
region: eu-central-1
filters:
"tag:Environment": "production"
"instance-state-name": "running"
register: ec2_instances
- name: Build Inventory
set_fact:
dynamic_inventory:
all:
hosts: "{{ ec2_instances.instances | map(attribute='private_ip_address') | list }}"
- name: Write Inventory to Artifacts
copy:
content: "{{ dynamic_inventory | to_nice_yaml }}"
dest: "{{ workspace.config.artifacts_root }}/blocks/{{ block.name }}/inventory.yml"
Rollback playbook
# rollback.yml
---
- name: Rollback Kubernetes Deployment
hosts: localhost
gather_facts: no
tasks:
- name: Get Deployment History
kubernetes.core.k8s_info:
kind: Deployment
namespace: "{{ block.config.namespace }}"
name: "{{ block.config.app.name }}"
register: deployment
- name: Rollback to Previous Revision
kubernetes.core.k8s:
state: present
kind: Deployment
namespace: "{{ block.config.namespace }}"
name: "{{ block.config.app.name }}"
definition:
spec:
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: "{{ ansible_date_time.iso8601 }}"
when: deployment.resources | length > 0
- name: Wait for Rollback
kubernetes.core.k8s_info:
kind: Deployment
namespace: "{{ block.config.namespace }}"
name: "{{ block.config.app.name }}"
register: deployment_status
until: deployment_status.resources[0].status.updatedReplicas == block.config.app.replicas
retries: 30
delay: 10
Advanced Docker examples
Multi-container applications
Orchestrating complex multi-container stacks with Ansible:
# block.poly
name: microservices-docker
kind: dockerapp
config:
app:
frontend:
image: myapp-frontend:latest
port: 3000
backend:
image: myapp-backend:latest
port: 8080
database:
image: postgres:14
user: appuser
password: "{{ vault_db_password }}"
database: appdb
redis:
image: redis:7-alpine
nginx:
port: 80
actions:
- name: install
playbook: docker-deploy.yml
- name: update
playbook: docker-update.yml
- name: backup
playbook: docker-backup.yml
- name: uninstall
playbook: docker-down.yml
Deployment playbook:
# docker-deploy.yml
---
- name: Deploy Microservices Stack
hosts: all
become: yes
tasks:
- name: Create application directory structure
file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- "/opt/{{ block.name }}"
- "/opt/{{ block.name }}/nginx"
- "/opt/{{ block.name }}/data/postgres"
- "/opt/{{ block.name }}/data/redis"
- name: Copy NGINX configuration
template:
src: templates/nginx.conf.j2
dest: "/opt/{{ block.name }}/nginx/nginx.conf"
notify: Reload NGINX
- name: Copy docker-compose file
template:
src: templates/docker-compose.yml.j2
dest: "/opt/{{ block.name }}/docker-compose.yml"
- name: Copy environment file
template:
src: templates/env.j2
dest: "/opt/{{ block.name }}/.env"
mode: '0600'
- name: Pull latest images
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
pull: yes
register: pull_result
- name: Start Docker Compose stack
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
state: present
remove_orphans: yes
register: compose_result
- name: Wait for backend to be healthy
uri:
url: "http://localhost:{{ block.config.app.backend.port }}/health"
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 5
- name: Wait for frontend to be healthy
uri:
url: "http://localhost:{{ block.config.app.frontend.port }}"
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 5
handlers:
- name: Reload NGINX
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
services:
- nginx
restarted: yes
Docker Compose template:
# templates/docker-compose.yml.j2
version: '3.8'
services:
frontend:
image: {{ block.config.app.frontend.image }}
container_name: {{ block.name }}-frontend
ports:
- "{{ block.config.app.frontend.port }}:3000"
environment:
- BACKEND_URL=http://backend:8080
- NODE_ENV=production
depends_on:
- backend
networks:
- app-network
restart: unless-stopped
backend:
image: {{ block.config.app.backend.image }}
container_name: {{ block.name }}-backend
ports:
- "{{ block.config.app.backend.port }}:8080"
environment:
- DATABASE_URL=postgresql://{{ block.config.database.user }}:{{ block.config.database.password }}@database:5432/{{ block.config.database.database }}
- REDIS_URL=redis://redis:6379
depends_on:
- database
- redis
networks:
- app-network
restart: unless-stopped
database:
image: {{ block.config.database.image }}
container_name: {{ block.name }}-database
environment:
- POSTGRES_USER={{ block.config.database.user }}
- POSTGRES_PASSWORD={{ block.config.database.password }}
- POSTGRES_DB={{ block.config.database.database }}
volumes:
- /opt/{{ block.name }}/data/postgres:/var/lib/postgresql/data
networks:
- app-network
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ block.config.database.user }}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: {{ block.config.redis.image }}
container_name: {{ block.name }}-redis
volumes:
- /opt/{{ block.name }}/data/redis:/data
networks:
- app-network
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: {{ block.name }}-nginx
ports:
- "{{ block.config.nginx.port }}:80"
volumes:
- /opt/{{ block.name }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- frontend
- backend
networks:
- app-network
restart: unless-stopped
networks:
app-network:
driver: bridge
volumes:
postgres-data:
redis-data:
Docker image building with Ansible
# docker-build-and-push.yml
---
- name: Build and Push Docker Images
hosts: localhost
gather_facts: no
tasks:
- name: Build Frontend Image
community.docker.docker_image:
name: "{{ block.config.app.frontend.image }}"
source: build
build:
path: "{{ workspace.root }}/src/frontend"
dockerfile: Dockerfile
args:
NODE_ENV: production
push: yes
force_source: yes
- name: Build Backend Image
community.docker.docker_image:
name: "{{ block.config.app.backend.image }}"
source: build
build:
path: "{{ workspace.root }}/src/backend"
dockerfile: Dockerfile
push: yes
force_source: yes
- name: Tag images with version
community.docker.docker_image:
name: "{{ item.name }}"
repository: "{{ item.name }}"
tag: "{{ block.version }}"
source: local
push: yes
loop:
- { name: "{{ block.config.app.frontend.image }}" }
- { name: "{{ block.config.app.backend.image }}" }
Docker backup and restore
# docker-backup.yml
---
- name: Backup Docker Volumes
hosts: all
become: yes
vars:
backup_dir: "/backup/{{ block.name }}"
backup_timestamp: "{{ ansible_date_time.epoch }}"
tasks:
- name: Create backup directory
file:
path: "{{ backup_dir }}"
state: directory
mode: '0700'
- name: Stop containers for consistent backup
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
stopped: yes
when: block.config.backup.stop_containers | default(true)
- name: Backup PostgreSQL data
archive:
path: "/opt/{{ block.name }}/data/postgres"
dest: "{{ backup_dir }}/postgres-{{ backup_timestamp }}.tar.gz"
format: gz
- name: Backup Redis data
archive:
path: "/opt/{{ block.name }}/data/redis"
dest: "{{ backup_dir }}/redis-{{ backup_timestamp }}.tar.gz"
format: gz
- name: Create PostgreSQL dump
community.docker.docker_container_exec:
container: "{{ block.name }}-database"
command: >
pg_dump -U {{ block.config.database.user }}
{{ block.config.database.database }}
register: pg_dump
- name: Save PostgreSQL dump
copy:
content: "{{ pg_dump.stdout }}"
dest: "{{ backup_dir }}/database-{{ backup_timestamp }}.sql"
- name: Restart containers
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
state: present
when: block.config.backup.stop_containers | default(true)
- name: Clean old backups (keep last 7)
shell: |
cd {{ backup_dir }}
ls -t | tail -n +15 | xargs -r rm
# docker-restore.yml
---
- name: Restore Docker Volumes
hosts: all
become: yes
vars:
backup_dir: "/backup/{{ block.name }}"
tasks:
- name: List available backups
find:
paths: "{{ backup_dir }}"
patterns: "postgres-*.tar.gz"
register: backup_files
- name: Get latest backup
set_fact:
latest_backup: "{{ backup_files.files | sort(attribute='mtime') | last }}"
- name: Stop Docker Compose stack
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
state: absent
- name: Remove old data
file:
path: "/opt/{{ block.name }}/data/postgres"
state: absent
- name: Extract backup
unarchive:
src: "{{ latest_backup.path }}"
dest: "/opt/{{ block.name }}/data/"
remote_src: yes
- name: Start Docker Compose stack
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
state: present
Docker health checks and monitoring
# docker-health-check.yml
---
- name: Check Docker Container Health
hosts: all
become: yes
tasks:
- name: Get container status
community.docker.docker_container_info:
name: "{{ item }}"
register: container_info
loop:
- "{{ block.name }}-frontend"
- "{{ block.name }}-backend"
- "{{ block.name }}-database"
- "{{ block.name }}-redis"
- "{{ block.name }}-nginx"
- name: Check if all containers are running
assert:
that:
- item.container.State.Running
fail_msg: "Container {{ item.item }} is not running!"
loop: "{{ container_info.results }}"
- name: Check container health
assert:
that:
- item.container.State.Health.Status == "healthy" or item.container.State.Health is not defined
fail_msg: "Container {{ item.item }} is unhealthy!"
loop: "{{ container_info.results }}"
- name: Check resource usage
community.docker.docker_container_info:
name: "{{ item }}"
register: resource_info
loop:
- "{{ block.name }}-backend"
- "{{ block.name }}-database"
- name: Display resource usage
debug:
msg: "{{ item.container.Name }}: CPU: {{ item.container.HostConfig.CpuShares }}, Memory: {{ item.container.HostConfig.Memory }}"
loop: "{{ resource_info.results }}"
- name: Check disk usage of volumes
shell: |
du -sh /opt/{{ block.name }}/data/*
register: disk_usage
- name: Display disk usage
debug:
var: disk_usage.stdout_lines
Zero-downtime updates
# docker-update.yml
---
- name: Zero-Downtime Docker Update
hosts: all
become: yes
tasks:
- name: Pull latest images
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
pull: yes
register: pull_result
- name: Check if images were updated
set_fact:
images_updated: "{{ pull_result.changed }}"
- name: Create backup before update
include_tasks: docker-backup.yml
when: images_updated
- name: Update backend (rolling)
block:
- name: Scale backend to 2 replicas
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
services:
- backend
scale:
backend: 2
- name: Wait for new backend to be healthy
uri:
url: "http://localhost:{{ block.config.app.backend.port }}/health"
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 5
- name: Remove old backend container
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
services:
- backend
scale:
backend: 1
when: images_updated
- name: Update frontend (rolling)
block:
- name: Scale frontend to 2 replicas
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
services:
- frontend
scale:
frontend: 2
- name: Wait for new frontend to be healthy
uri:
url: "http://localhost:{{ block.config.app.frontend.port }}"
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 5
- name: Remove old frontend container
community.docker.docker_compose:
project_src: "/opt/{{ block.name }}"
services:
- frontend
scale:
frontend: 1
when: images_updated
- name: Verify all services are running
include_tasks: docker-health-check.yml
Docker network management
# docker-network-setup.yml
---
- name: Setup Docker Networks
hosts: all
become: yes
tasks:
- name: Create custom networks
community.docker.docker_network:
name: "{{ item.name }}"
driver: "{{ item.driver | default('bridge') }}"
ipam_config:
- subnet: "{{ item.subnet }}"
state: present
loop: "{{ block.config.networks }}"
- name: Connect containers to networks
community.docker.docker_container:
name: "{{ item.container }}"
networks:
- name: "{{ item.network }}"
state: started
loop: "{{ block.config.container_network_mapping }}"
Best practices for Ansible with Polycrate
1. Write idempotent playbooks
Make sure playbooks can be executed multiple times without unwanted side effects:
- name: Ensure directory exists
file:
path: "/opt/{{ block.name }}"
state: directory # Idempotent
- name: Ensure service is running
service:
name: myapp
state: started # Idempotent
2. Error handling
- name: Deploy with error handling
block:
- name: Deploy application
kubernetes.core.k8s:
state: present
template: deployment.yml.j2
rescue:
- name: Rollback on failure
kubernetes.core.k8s:
state: absent
name: "{{ block.config.app.name }}"
- name: Notify team
uri:
url: "{{ block.config.slack_webhook }}"
method: POST
body_format: json
body:
text: "Deployment of {{ block.name }} failed!"
always:
- name: Clean up temporary files
file:
path: "/tmp/{{ block.name }}"
state: absent
3. Use Ansible tags
# install.yml
- name: Complete Installation
hosts: all
tasks:
- name: Install dependencies
tags: [dependencies, install]
apt:
name: "{{ item }}"
loop: "{{ block.config.dependencies }}"
- name: Deploy application
tags: [deploy, install]
include_tasks: deploy.yml
- name: Configure monitoring
tags: [monitoring]
include_tasks: monitoring.yml
# Run only specific tags
polycrate run my-app install -- --tags deploy
4. Use Ansible facts
- name: Use gathered facts
hosts: all
gather_facts: yes
tasks:
- name: Install packages based on OS
package:
name: "{{ item }}"
loop: "{{ block.config.packages[ansible_os_family] }}"
- name: Configure based on available memory
template:
src: config.j2
dest: /etc/app/config.yml
vars:
memory_limit: "{{ (ansible_memtotal_mb * 0.8) | int }}Mi"
5. Handle secrets securely
# Never put secrets directly in block.poly
# ❌ Bad:
config:
database:
password: "mysecretpassword"
# ✅ Good: use Ansible Vault
config:
database:
password: "{{ vault_db_password }}"
# Use Vault-encrypted secrets
polycrate run my-app install --ask-vault-pass