Skip to content

Repository files navigation

SpecsOps Ansible Collection

Ansible collection specsnl.specsops — reusable roles for provisioning and hardening Ubuntu 26.04 (Resolute) hosts at Specs.

Roles

Role Description Docs
specsnl.specsops.base apt update/upgrade, core packages, locale, timezone, sysctl roles/base
specsnl.specsops.hardening sshd drop-in hardening + fail2ban roles/hardening
specsnl.specsops.firewall ufw baseline + parameterized extra rules roles/firewall
specsnl.specsops.unattended_upgrades chrony + unattended-upgrades + apt config roles/unattended_upgrades
specsnl.specsops.postgresql PGDG repo, PostgreSQL install, tuning, pg_hba, ufw port roles/postgresql
specsnl.specsops.swap swap file create/format/persist/activate + sysctl roles/swap
specsnl.specsops.logrotate global logrotate maxsize + compression roles/logrotate
specsnl.specsops.cleanup apt autoremove/clean, wipe temp dirs (build-time) roles/cleanup
specsnl.specsops.podman Podman from the Ubuntu universe repo (Quadlet included) roles/podman
specsnl.specsops.caddy Cloudsmith apt repo, Caddy install, service, ufw 80/443 roles/caddy
specsnl.specsops.specsdeployd deploy receiver scaffolding; binary install is opt-in roles/specsdeployd
specsnl.specsops.ansible_pull ansible-pull service + timer, disabled and inert by default roles/ansible_pull
specsnl.specsops.cloud_init_user cloud-init default user: non-root sudo login for images roles/cloud_init_user

Every role documents its variables in its own README; docs/README.md carries the condensed index and the notes on container safety.

All roles detect container environments and skip the steps that cannot work there (ufw enable, systemctl start, swapon, sysctl reload), so the same roles run unchanged in a Packer build container, in Molecule, and on a live VM.

Requirements

  • Ansible Core >= 2.16
  • Ubuntu 26.04 LTS (Resolute Raccoon)
  • Collections: community.general, ansible.posix (installed automatically via Galaxy)

Installation

# requirements.yml
collections:
  - name: git+https://github.com/specsnl/specsops-ansible-collection.git
    type: git
    version: main
ansible-galaxy collection install -r requirements.yml

Usage

- hosts: all
  become: true
  roles:
    - specsnl.specsops.base
    - specsnl.specsops.hardening
    - specsnl.specsops.firewall
    - specsnl.specsops.unattended_upgrades
    - specsnl.specsops.swap
    - specsnl.specsops.logrotate

If you use both base and swap, note that they each manage vm.swappiness in their own /etc/sysctl.d/ drop-in. Set it in one of them, not both.

The app-image roles layer on top. Order matters twice: firewall before caddy, which skips its ufw rules with a warning when ufw is absent, and podman before specsdeployd, whose sudoers drop-in escalates to podman pull.

- hosts: app
  become: true
  roles:
    - specsnl.specsops.base
    - specsnl.specsops.firewall
    - specsnl.specsops.podman
    - specsnl.specsops.caddy
    - specsnl.specsops.specsdeployd
    - specsnl.specsops.ansible_pull

specsdeployd and ansible_pull ship inert by default — no binary and no repository — so the same playbook bakes a golden image and provisions a live host. Set specsdeployd_version and ansible_pull_repo to make either one live.

Development

Everything runs in containers via Task and Docker Compose — you need Docker and task on your machine, but no local Ansible, Python or Molecule. The ansible service (ghcr.io/specsnl/ansible) mounts the repo at /workspace and the Docker socket, so Molecule can start test containers from inside it.

# Lint (yamllint + ansible-lint)
task ansible:lint
task ansible:lint:ansible:fix   # auto-fix what ansible-lint can

# Test a single role (destroy → syntax → create → converge → idempotence → verify → destroy)
task ansible:test:base

# Test all roles
task ansible:test

# Iterate on one role without tearing the container down
task ansible:converge:base
task ansible:verify:base
task ansible:destroy:base

# Markdown
task md:checkstyle
task md:fixstyle

# Interactive shell in the Ansible container
task shell

# Build / install the collection tarball into ./dist
task galaxy:build
task galaxy:install:local

task --list shows every task.

Molecule tests run against geerlingguy/docker-ubuntu2604-ansible, pinned by digest, with systemd as PID 1.

Local testing with Podman

The Molecule driver is docker, and compose.yml bind-mounts /var/run/docker.sock into the ansible service. With Podman you need a Docker-API-compatible socket at that path, so run Compose against the Podman socket and expose it:

podman system service --time=0 &
export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock
task ansible:test:base

If the socket lives elsewhere, override the bind mount in a compose.override.yml rather than editing compose.yml.

Release

  1. Add a changelog fragment under changelogs/fragments/
  2. Bump version: in galaxy.yml
  3. Run task changelog:release:<version> — regenerates CHANGELOG.rst and consumes the fragments
  4. Commit, tag <version>, push

Tags carry no v prefix — 0.1.0, not v0.1.0 — matching the upstream Ansible collections. The Tags must not have v-prefix ruleset under .github/rulesets/ enforces this.

The tag workflow first asserts that the GALAXY_API_KEY secret is set and that the tag matches version: in galaxy.yml, reporting both problems at once if both are wrong. It then builds the tarball and publishes it to Ansible Galaxy.

CI

Workflow Trigger Runs
pr.yml pull request Lint + Molecule, only for the roles the PR touches
main.yml push to main Lint + Molecule for all roles
md.yml **.md changes markdownlint + table formatting
tag.yml X.Y.Z tag Build + publish to Ansible Galaxy

.github/rulesets/ holds snapshots of the repository rulesets. They are not applied automatically — import them under Settings → Rules → Rulesets. Main requires a pull request and the Check and Markdown gate jobs; the other jobs are conditional and would deadlock a PR that does not touch their paths.

Releases

Used by

Contributors

Languages