remote-backups.comremote-backups.com
Contact illustration
Sign In
Don't have an account ?Sign Up

PBS + Ansible: Automate Your Backup Infrastructure

Manual PBS configuration is fine for one node. The moment you're managing multiple Proxmox clusters, separate client environments, or need a teammate to reproduce your setup from scratch, the cracks appear fast. This post walks through structuring an Ansible project that provisions and manages Proxmox Backup Server — datastores, API tokens, retention policies, and offsite sync — as repeatable, auditable code.

Key Takeaways
  • Ansible wraps the proxmox-backup-manager CLI in idempotent tasks — same result on every run
  • Group vars define shared retention and scheduling defaults; host vars handle per-client overrides
  • API tokens should be created through Ansible and stored in Vault, not copied into a spreadsheet
  • Pruning schedules and GC cadence belong in version control, not clicked through the web UI
  • Offsite sync jobs to remote-backups.com are provisioned the same way as any other PBS resource

Why Manual PBS Config Breaks at Scale

Three nodes in, manual configuration produces drift. Node A has a 30-day retention policy. Node B got updated last month and is on 60 days. Node C has an API token nobody can identify, from a client that left two years ago.

This isn't an edge case — it's what happens when configuration lives in browser tabs and memory.

The specific failure modes:

  • Drift: Each node diverges quietly. There's no source of truth, so you only discover the divergence when something breaks.
  • Undocumented tokens: Tokens created manually rarely get documented. When you rotate credentials, you find out the hard way which services depended on the old ones.
  • Forgotten retention policies: One misconfigured datastore and you're either filling disks or losing more history than your clients expect. Both are bad at 2am.
  • Onboarding friction: Provisioning a new client environment manually takes an hour per node. With Ansible, it takes one command.

Ansible addresses this with three properties: idempotency (run it 10 times, same result), version control (git diff your entire config), and repeatability (new node gets the exact same baseline as every other). None of that is exotic. It just requires putting in the structure upfront.

Inventory and Variable Structure

A clean inventory layout separates PBS hosts from the variables that configure them. For a multi-client or multi-node setup, this structure scales without becoming a maze:

text
inventory/
├── hosts.yml
├── group_vars/
│   ├── all.yml
│   └── pbs_servers.yml
└── host_vars/
    ├── pbs-node-01.yml
    └── pbs-node-02.yml
Recommended inventory layout

group_vars/pbs_servers.yml holds defaults that apply to every PBS node: datastore base path, GC schedule, and your baseline retention policy.

yaml
pbs_datastore_base: /mnt/datastore
pbs_gc_schedule: "daily"
pbs_prune_schedule: "daily"

pbs_retention:
  keep_last: 3
  keep_daily: 14
  keep_weekly: 8
  keep_monthly: 6
  keep_yearly: 1
group_vars/pbs_servers.yml

host_vars/pbs-node-02.yml overrides only what's different for a specific node or client. Everything else falls through to the group default.

yaml
# Client with a longer SLA — override retention only
pbs_retention:
  keep_last: 5
  keep_daily: 30
  keep_weekly: 12
  keep_monthly: 12
  keep_yearly: 2
host_vars/pbs-node-02.yml

hosts.yml defines the group and connection details:

yaml
all:
  children:
    pbs_servers:
      hosts:
        pbs-node-01:
          ansible_host: 10.0.1.10
          ansible_user: root
        pbs-node-02:
          ansible_host: 10.0.1.11
          ansible_user: root
inventory/hosts.yml

Change a retention default in group_vars once, and it propagates to every node on the next run. Per-client exceptions stay in host_vars, isolated from shared defaults.

Datastore Provisioning Playbook

Provisioning a Proxmox Backup Server datastore means: create the directory, register it with PBS via proxmox-backup-manager, then apply the pruning and GC schedule. All three steps in one playbook:

yaml
- name: Provision PBS datastore
  hosts: pbs_servers
  become: true
  tasks:
    - name: Create datastore directory
      ansible.builtin.file:
        path: "{{ pbs_datastore_base }}/{{ datastore_name }}"
        state: directory
        owner: backup
        group: backup
        mode: '0750'

    - name: Register datastore with PBS
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager datastore create {{ datastore_name }}
          {{ pbs_datastore_base }}/{{ datastore_name }}
      register: ds_create
      failed_when: ds_create.rc != 0 and "already exists" not in ds_create.stderr
      changed_when: ds_create.rc == 0

    - name: Apply GC schedule
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager datastore update {{ datastore_name }}
          --gc-schedule "{{ pbs_gc_schedule }}"
      changed_when: true
      tags: [retention]

    - name: Apply prune schedule
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager datastore update {{ datastore_name }}
          --prune-schedule "{{ pbs_prune_schedule }}"
      changed_when: true
      tags: [retention]
playbooks/provision-datastores.yml
Handle the 'already exists' exit code

proxmox-backup-manager returns a non-zero exit code when a resource already exists. Without the failed_when and changed_when conditions shown above, your playbook fails on every run after the first. Always check stderr for "already exists" when wrapping PBS CLI commands.

Pass datastore_name as an extra variable (-e datastore_name=client-a) or define it in each host_vars file for fully automated multi-client provisioning.

Token and ACL Management

Every client or service connecting to PBS needs an API token. Creating them manually means you end up with tokens named "test-token-FINAL-v2" and no record of what they're for or when they were created.

The better approach: create tokens through Ansible and store secrets in Ansible Vault immediately at creation time.

yaml
- name: Manage PBS API tokens
  hosts: pbs_servers
  become: true
  vars_files:
    - ../vault/pbs_tokens.yml
  tasks:
    - name: Create PBS user for client
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager user create {{ pbs_client_user }}@pbs
          --comment "{{ pbs_client_comment | default('Managed by Ansible') }}"
      register: user_create
      failed_when: user_create.rc != 0 and "already exists" not in user_create.stderr
      changed_when: user_create.rc == 0

    - name: Create API token with known secret
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager user token create
          {{ pbs_client_user }}@pbs {{ token_name }}
          --token-secret "{{ vault_token_secret }}"
      register: token_create
      failed_when: token_create.rc != 0 and "already exists" not in token_create.stderr
      changed_when: token_create.rc == 0

    - name: Assign DatastoreBackup ACL
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager acl update
          /datastore/{{ datastore_name }}
          --auth-id "{{ pbs_client_user }}@pbs!{{ token_name }}"
          --role DatastoreBackup
      changed_when: true
playbooks/manage-tokens.yml

The --token-secret flag lets you supply a pre-generated secret rather than capturing the one-time output from PBS. Pre-generate secrets with openssl rand -hex 32, store in vault/pbs_tokens.yml, then encrypt: ansible-vault encrypt vault/pbs_tokens.yml.

Roles: DatastoreBackup vs DatastoreReader

DatastoreBackup gives write and read access — right for clients sending backups. DatastoreReader gives read-only access — right for restore tokens and monitoring. Assign the minimum role needed. The offsite sync token only needs DatastoreReader on the source.

For MSPs, define pbs_client_user, token_name, and vault_token_secret in each host_vars file. Each client gets their own isolated user and token, with ACL scoped to their specific datastore.

Pruning and Retention as Code

Retention policy is among the most drift-prone parts of PBS. Clients have different SLAs, and without code, it's impossible to audit what's actually configured across nodes.

Define retention in variables and apply via datastore update. The playbook reuses the same task structure:

yaml
- name: Apply retention policy to datastore
  hosts: pbs_servers
  become: true
  tasks:
    - name: Set retention flags
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager datastore update {{ datastore_name }}
          --keep-last {{ pbs_retention.keep_last | default(omit) }}
          --keep-daily {{ pbs_retention.keep_daily | default(omit) }}
          --keep-weekly {{ pbs_retention.keep_weekly | default(omit) }}
          --keep-monthly {{ pbs_retention.keep_monthly | default(omit) }}
          --keep-yearly {{ pbs_retention.keep_yearly | default(omit) }}
      changed_when: true
      tags: [retention]
playbooks/apply-retention.yml

Here's the full reference for PBS retention flags:

PBS Retention Flag Reference
Flag
--keep-last
--keep-hourly
--keep-daily
--keep-weekly
--keep-monthly
--keep-yearly
Description
Keep the N most recent snapshots
Keep one snapshot per clock-hour
Keep one snapshot per calendar day
Keep one snapshot per calendar week
Keep one snapshot per calendar month
Keep one snapshot per calendar year
Example Value
3
24
14
8
6
2
What It Keeps
Latest 3 snapshots, regardless of age
One per hour for the last 24 hours
One per day for the last 2 weeks
One per week for the last 8 weeks
One per month for the last 6 months
One per year for the last 2 years

Flags combine. A policy of keep-last: 3, keep-daily: 14, keep-weekly: 8 keeps the three most recent snapshots, plus daily coverage for two weeks, plus weekly snapshots for two months. Snapshots matching any criterion are retained.

For MSPs: set tighter defaults in group_vars and only relax them in host_vars for clients with longer SLAs. This prevents accidental over-retention from quietly filling disks.

Offsite Sync to remote-backups.com

A complete infrastructure-as-code setup includes the offsite leg. remote-backups.com provides a hosted Proxmox Backup Server endpoint. You add it as a remote in PBS and create a sync job — the same way you'd configure any other PBS sync job.

First, add the remote. Store credentials in Vault:

yaml
- name: Configure offsite PBS sync
  hosts: pbs_servers
  become: true
  vars_files:
    - ../vault/remote_backups.yml
  tasks:
    - name: Add remote-backups.com as PBS remote
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager remote create remote-backups
          --url "https://pbs.remote-backups.com:8007"
          --username "{{ vault_rb_username }}"
          --password "{{ vault_rb_token_secret }}"
          --fingerprint "{{ rb_server_fingerprint }}"
      register: remote_create
      failed_when: remote_create.rc != 0 and "already exists" not in remote_create.stderr
      changed_when: remote_create.rc == 0

    - name: Create daily offsite sync job
      ansible.builtin.command:
        cmd: >
          proxmox-backup-manager sync-job create offsite-daily-{{ datastore_name }}
          --store {{ datastore_name }}
          --remote remote-backups
          --remote-store {{ vault_rb_datastore }}
          --schedule "daily"
          --remove-vanished false
          --encrypted-only true
      register: sync_create
      failed_when: sync_create.rc != 0 and "already exists" not in sync_create.stderr
      changed_when: sync_create.rc == 0
playbooks/configure-offsite.yml

--remove-vanished false preserves backups on the remote even after local pruning removes them. This gives you independent retention history on the offsite copy. --encrypted-only true ensures no plaintext chunks leave your environment — only data encrypted with your client-side keys gets synced.

Always use --encrypted-only for external targets

Without this flag, PBS will sync any chunk — including unencrypted ones. If clients don't all use client-side encryption, this can expose plaintext backup data to the remote target. Enable client-side encryption at the PVE level first, then enforce it on the sync job.

Store vault_rb_username, vault_rb_token_secret, and vault_rb_datastore in vault/remote_backups.yml, encrypted with Ansible Vault. The fingerprint is not sensitive and goes in group_vars or host_vars.

Once these tasks run, every new PBS node you provision gets the offsite sync job configured identically. No manual steps, no nodes that get missed at 3am.

Testing and Idempotency

Before running playbooks against production PBS nodes, verify they behave the way you expect.

--check mode runs tasks without applying changes:

bash
ansible-playbook playbooks/provision-datastores.yml \
  -e datastore_name=test-ds \
  --check --diff
Dry-run a playbook

Not all tasks support check mode cleanly — ansible.builtin.command tasks with changed_when: true will always report changed. For real idempotency verification, run the playbook twice on a test node. The second run should report zero changes. If tasks still show changed, your changed_when conditions need work.

A cleaner alternative to error-string matching is querying datastore state before creating:

yaml
- name: Get existing datastores
  ansible.builtin.command:
    cmd: proxmox-backup-manager datastore list --output-format json
  register: existing_datastores
  changed_when: false

- name: Create datastore only if missing
  ansible.builtin.command:
    cmd: >
      proxmox-backup-manager datastore create {{ datastore_name }}
      {{ pbs_datastore_base }}/{{ datastore_name }}
  when: >
    datastore_name not in
    (existing_datastores.stdout | from_json | map(attribute='name') | list)
Idempotent datastore task using JSON query

This makes the logic explicit rather than relying on exit code parsing. It's also safer: you won't accidentally skip a genuine failure because the error message happened to contain "already exists".

Tag tasks for targeted runs

Add tags to task blocks — tags: [datastores], tags: [tokens], tags: [retention]. This lets you run --tags retention when updating pruning policies without re-running the full provisioning stack. It speeds up day-to-day operations and reduces the blast radius of any individual change.

For MSPs managing multiple client environments: structure your playbooks so each client is a host_vars file. Adding a new client is copy the template, edit the variables, run the playbook. Every new client gets the full baseline — datastore, token, ACL, retention, offsite sync — in one pass.

Wrapping Up

Manual PBS configuration works until it doesn't. Past two or three nodes, undocumented state and configuration drift start costing real time. Ansible wraps proxmox-backup-manager in idempotent tasks, puts your retention policies and token management into version control, and makes onboarding a new PBS node a single command.

The patterns here — inventory structure, Vault for secrets, idempotent CLI wrapping — apply equally to local datastores and offsite sync targets. Once your PBS infrastructure is defined as code, you can audit it, hand it off to a teammate, and rebuild from scratch if you need to.

Need the offsite leg handled for you?

remote-backups.com provides encrypted PBS targets in EU datacenters — isolated credentials, no shared infrastructure, and full PBS compatibility. Provision it with Ansible on day one.

View Plans

No native Ansible module exists for PBS as of early 2026. The standard approach is wrapping the proxmox-backup-manager CLI with ansible.builtin.command or ansible.builtin.shell, combined with careful failed_when and changed_when conditions for idempotency.

Use the --token-secret flag when creating tokens so you supply the secret rather than capture it. Pre-generate secrets with 'openssl rand -hex 32', store them in an Ansible Vault-encrypted file, and reference them in your token creation tasks.

Yes. The proxmox-backup-manager CLI is the same on a standalone PBS install. The Ansible tasks in this post connect over SSH and run CLI commands — they don't depend on PVE being present.

PBS sync jobs use a pull model: configure them on the destination PBS. The destination connects to the source (configured as a 'remote') and pulls snapshots. For syncing to remote-backups.com, the sync job is configured on the remote-backups.com side — you provide your local PBS connection details when setting up the service.

Root is the simplest option for PBS hosts, as proxmox-backup-manager requires root or a user in the backup group for most operations. If you need privilege separation, configure sudo for specific commands and use become: true with a non-root ansible_user.
Bennet Gallein
Bennet Gallein

remote-backups.com operator

Infrastructure enthusiast and founder of remote-backups.com. I build and operate reliable backup infrastructure powered by Proxmox Backup Server, so you can focus on what matters most: your data staying safe.