Nginx and Let's Encrypt as a Reusable Polycrate Block
Fabian Peter 9 Minuten Lesezeit

Nginx and Let’s Encrypt as a Reusable Polycrate Block

Building and sharing Nginx and Let’s Encrypt as a reusable Polycrate block
Ganze Serie lesen (24 Artikel)

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.

  1. Install Polycrate and Build Your First Ansible Block in 15 Minutes
  2. Blocks, Actions, and Workspaces: The Modular Principle of Polycrate
  3. Linux Servers on Autopilot: System Management with Polycrate and Ansible
  4. Nginx and Let's Encrypt as a Reusable Polycrate Block
  5. Managing Docker Stacks on Linux Servers with Polycrate
  6. Many Servers, One Truth: Multi-Server Management with Polycrate Inventories
  7. Windows Automation with Polycrate: Ansible and WinRM Without Pain
  8. Windows Software Deployment without SCCM: Chocolatey and Ansible
  9. Hybrid Automation: Windows and Linux in the Same Polycrate Workspace
  10. Deploy Kubernetes Apps from the PolyHub: From Idea to Deployment in Minutes
  11. Creating Your Own Kubernetes App as a Polycrate Block: A Step-by-Step Guide
  12. Multi-Cluster Kubernetes with Polycrate: Why One Cluster, One Workspace
  13. SSH Sessions and kubectl Debugging: Polycrate as an Operations Tool
  14. Helm Charts as a Polycrate Block: More Control Over Chart Deployments
  15. Policy as Code: Automating Compliance Requirements with Polycrate
  16. Workspace Encryption: Managing Secrets in GDPR Compliance – Without External Tooling
  17. Managing Raspberry Pi and Edge Nodes with Polycrate in IoT and Edge Computing
  18. Enterprise Automation: Building, Versioning, and Sharing Blocks Within Teams
  19. Polycrate MCP: Connecting AI Assistants with Live Infrastructure Context
  20. Polycrate vs. plain Ansible: What You Gain – and Why It's Worth It
  21. The Polycrate Ecosystem: PolyHub, API, MCP, and the Future of Automation
  22. Your First Productive Polycrate Workspace: A Checklist for Getting Started
  23. Auditable Operations: SSH Sessions and CLI Activities with Polycrate API
  24. Polycrate API for Teams: Centralized Monitoring and Remote Triggering

TL;DR

  • You build a reusable Polycrate block that automates the deployment of Nginx and Let’s Encrypt (via community.general.certbot) on a Linux server – including a generic nginx.conf as a Jinja2 template.
  • The block parameterizes domain, contact email, and upstream port through block.config.* and can be used in any workspace with different values – without code duplication or wiki tinkering.
  • Polycrate containerizes Ansible and the entire toolchain: no local Ansible, no Python chaos, no individual setups – the same block runs identically on every developer workstation.
  • Through a registry (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.
  • ayedo supports teams in building such standardized, shareable automation blocks – from designing the block structure to operating a registry and integrating into existing processes.

The Real Problem: The Forgotten Nginx Config

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.

  • Why is the upstream port 8081?
  • Why is a subdomain being redirected here?
  • Which certbot option was it again?

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:

  • Managing local Ansible installation, Python versions, collections
  • Maintaining, distributing, versioning roles and playbooks
  • Colleagues need to understand Ansible CLI, inventories, and variable structures

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.


From Server Config to Reusable Block

Polycrate builds directly on Ansible but solves two typical problems:

  1. 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.

  2. 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:

  • Installs Nginx and certbot on Ubuntu 22.04
  • Obtains a Let’s Encrypt certificate for a domain
  • Deploys an nginx.conf based on a Jinja2 template
  • Parameterizes:
    • block.config.domain
    • block.config.email
    • block.config.upstream_port

Block Structure: Directory & block.poly

Starting point is an existing Polycrate workspace, e.g.:

# workspace.poly
name: acme-corp-automation
organization: acme

Our block is located in ./blocks/nginx-letsencrypt/ and contains:

  • block.poly
  • nginx-letsencrypt.yml (Ansible playbook)
  • nginx.conf.j2 (Jinja2 template)

Directory layout (overview)

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).

Complete block.poly

name: nginx-letsencrypt
version: 1.0.0
kind: generic

config:
  domain: ""
  email: ""
  upstream_port: 8080

actions:
  - name: deploy
    playbook: nginx-letsencrypt.yml

Key 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.com
    • email – contact email for Let’s Encrypt
    • upstream_port – port of the backend that Nginx proxies to (e.g., an app on 8080)
  • actions describes which playbooks are available.
    With polycrate run nginx-letsencrypt deploy, we execute the playbook.

Further details on block structure can be found in the official Block Concepts.


nginx.conf as Jinja2 Template

The core for reusability is the Nginx configuration as a template. Instead of fixed domain names and ports, we use Polycrate variables.

Complete 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.
  • The certificate paths point to certbot’s standard directories (/etc/letsencrypt/live/...).
  • The configuration is idempotently deployable from Ansible – changes to the template lead to a consistent state.

Ansible Playbook for Nginx + Let’s Encrypt

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.

Inventory in the Workspace

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: true

Complete Playbook nginx-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: true

Explanations:

  • 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).
  • We use the standalone authenticator:
    • Certbot briefly starts its own web server on port 80.
    • Then we deploy the HTTPS Nginx configuration.
  • Through vars, we bind the Polycrate variables (block.config.*) to Ansible variables, simplifying reading in the playbook.

Using the Block in the Workspace

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).

Referencing the Block in the Workspace

# 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: 8080
  • name: webserver is the instance name in the workspace.
  • The block lives in the registry at 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).
  • Under config, we override the default values from block.poly.

Execute Action

polycrate run webserver deploy

Polycrate:

  • Starts a container with Ansible and all necessary tools.
  • Loads inventory.yml from the workspace root.
  • Sets variables like block.config.* and executes the playbook in the container.
  • Modifies the target host via SSH – the container itself remains ephemeral.

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:

  • Ansible is installed locally
  • The correct Python version is running
  • The community.general collection is available

With Polycrate, these dependencies are part of the block or workspace and run in the container.


Pushing the Web Server Block to a Registry

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.

Adjust block.poly for the registry

Before 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.yml

Recommended: mirror the name in the directory layout, e.g. blocks/registry.acme-corp.com/infra/nginx/ (see Best practices).

Push the block

polycrate blocks push registry.acme-corp.com/infra/nginx

There 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:

  • Polycrate packages the block (including block.poly, playbook, template) as an OCI artifact.
  • The artifact is uploaded as registry.acme-corp.com/infra/nginx:1.0.0 (name + version as tag).
  • For a follow-up release, bump version in block.poly (e.g. 1.1.0) and push again.

In the workspace: pin the version

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).


Colleague Uses the Block in Their Workspace

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 at the Colleague

# 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: 9000

They don’t need to:

  • Read the playbook
  • Understand the Nginx template syntax
  • Know certbot options

They just set the configuration.

First polycrate run

Ähnliche Artikel