# How to Manage FreeBSD Servers with Ansible
Managing one FreeBSD server by hand is straightforward. Managing ten is tedious. Managing fifty without automation is a recipe for configuration drift, missed patches, and late-night firefighting. Ansible solves this problem cleanly, and it works with FreeBSD out of the box -- no agent installation required on your target hosts.
This guide covers everything from initial setup to production-ready playbooks for hardening, web server deployment, and jail management. Every example is copy-paste ready and tested against FreeBSD 14.x.
Why Ansible for FreeBSD
Ansible stands apart from other configuration management tools for several reasons that matter specifically to FreeBSD administrators.
**Agentless architecture.** Ansible connects to target hosts over SSH and executes tasks remotely. There is no daemon to install, no ports to open beyond SSH, and no package to keep updated on every managed server. This is a significant advantage on FreeBSD, where you want to keep your attack surface minimal and your base system clean.
**SSH-native.** FreeBSD ships with OpenSSH in the base system. Ansible uses this existing SSH infrastructure directly. If you can SSH into a server, Ansible can manage it. No additional transport layer, no proprietary protocol, no certificates to manage.
**Python-based.** Ansible modules execute Python on the remote host. FreeBSD includes Python in its ports and packages collection, and installing it takes a single pkg install command. Unlike tools that require Ruby or custom runtimes, the Python dependency is lightweight and well-supported on FreeBSD.
**FreeBSD-specific modules.** Ansible ships with modules purpose-built for FreeBSD: community.general.pkgng for package management, community.general.sysrc for rc.conf manipulation, and standard service module support for FreeBSD's rc.d init system. You are not fighting the tool to work around Linux assumptions.
**Idempotent by design.** Every Ansible module is designed to check current state before making changes. Running a playbook twice produces the same result as running it once. This is critical for FreeBSD servers in production where unintended changes cause outages.
Installing Ansible
On the Control Node
The control node is the machine you run Ansible from -- typically your workstation or a dedicated management server. Ansible runs on Linux, macOS, or FreeBSD itself.
On a FreeBSD control node:
bash
pkg install py311-ansible
On a Linux or macOS control node using pip:
bash
python3 -m pip install --user ansible
Verify the installation:
bash
ansible --version
You should see output showing Ansible core 2.16 or later.
On FreeBSD Target Hosts
Target hosts need only two things: SSH access and Python. SSH is already present in the FreeBSD base system. Install Python:
bash
pkg install python311
That is the only package Ansible requires on managed hosts. No agent, no daemon, no service to configure.
Inventory Setup
The inventory file tells Ansible which servers to manage and how to group them. Create a directory structure for your Ansible project:
bash
mkdir -p ~/ansible-freebsd/{inventory,playbooks,roles,group_vars,host_vars}
Create the inventory file at ~/ansible-freebsd/inventory/hosts.yml:
yaml
all:
children:
webservers:
hosts:
web01.example.com:
ansible_host: 203.0.113.10
web02.example.com:
ansible_host: 203.0.113.11
dbservers:
hosts:
db01.example.com:
ansible_host: 203.0.113.20
jailhosts:
hosts:
jail01.example.com:
ansible_host: 203.0.113.30
freebsd:
children:
webservers:
dbservers:
jailhosts:
The freebsd group is a parent group that contains all FreeBSD hosts. This lets you apply FreeBSD-specific variables to every host at once.
Group Variables
Create ~/ansible-freebsd/group_vars/freebsd.yml to set defaults for all FreeBSD hosts:
yaml
ansible_python_interpreter: /usr/local/bin/python3.11
ansible_become_method: su
ansible_become: true
ansible_user: admin
These four variables are critical for FreeBSD. Without them, Ansible will fail on its first connection attempt. The next section explains why.
Connection Configuration
FreeBSD differs from Linux in ways that affect Ansible's default behavior. You must configure these settings or every task will fail.
Python Interpreter Path
Linux distributions install Python at /usr/bin/python3. FreeBSD installs it at /usr/local/bin/python3.11 (or whichever version you installed). Ansible must know where to find Python on the remote host:
yaml
ansible_python_interpreter: /usr/local/bin/python3.11
Set this in your group variables for all FreeBSD hosts. Without it, Ansible will look for Python in the wrong location and every module will fail with a cryptic error.
Become Method
Linux servers typically use sudo for privilege escalation. FreeBSD includes su in the base system but does not include sudo by default. You have two options:
**Option 1: Use su (no extra packages)**
yaml
ansible_become_method: su
ansible_become_pass: "{{ vault_root_password }}"
This requires the root password, which you should store in Ansible Vault (covered later in this guide).
**Option 2: Install and use doas**
bash
pkg install doas
Configure /usr/local/etc/doas.conf:
permit nopass keepenv :wheel
Then set in your group variables:
yaml
ansible_become_method: doas
doas is a simpler alternative to sudo that originated in OpenBSD and is well-supported on FreeBSD. Many FreeBSD administrators prefer it over sudo for its smaller codebase and simpler configuration.
SSH Configuration
Create or update ~/.ssh/config on your control node for cleaner connections:
Host *.example.com
User admin
IdentityFile ~/.ssh/ansible_ed25519
StrictHostKeyChecking accept-new
The ansible.cfg File
Create ~/ansible-freebsd/ansible.cfg:
ini
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
vault_password_file = .vault_pass
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
[privilege_escalation]
become = True
become_method = su
become_user = root
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Enabling pipelining significantly speeds up Ansible on FreeBSD by reducing the number of SSH operations per task.
First Ad-Hoc Commands
Before writing playbooks, verify that Ansible can reach your FreeBSD hosts with ad-hoc commands.
Ping All Hosts
bash
ansible freebsd -m ping
Expected output:
web01.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
If this fails, check your Python interpreter path, SSH key, and become method settings.
Run a Shell Command
bash
ansible freebsd -m shell -a "freebsd-version"
This returns the FreeBSD version running on each host.
Check Installed Packages
bash
ansible webservers -m shell -a "pkg info | wc -l"
Install a Package
bash
ansible webservers -m community.general.pkgng -a "name=htop state=present"
Ad-hoc commands are useful for quick checks and one-off tasks. For repeatable configuration, use playbooks.
Writing Playbooks for FreeBSD
Playbooks are YAML files that describe the desired state of your servers. Here is the anatomy of a FreeBSD-aware playbook.
Package Management with pkgng
The community.general.pkgng module manages FreeBSD packages:
yaml
- name: Install common packages
hosts: freebsd
become: true
tasks:
- name: Ensure pkg is bootstrapped
raw: env ASSUME_ALWAYS_YES=yes pkg bootstrap
changed_when: false
- name: Install base packages
community.general.pkgng:
name:
- vim
- htop
- tmux
- curl
- rsync
- py311-pip
state: present
The raw task for bootstrapping pkg is important. On a fresh FreeBSD install, the pkg tool itself may not be initialized. The raw module executes a command without requiring Python, making it safe to run before anything else.
System Configuration with sysrc
The community.general.sysrc module manipulates /etc/rc.conf, which is the central configuration file for FreeBSD services:
yaml
- name: Configure system settings
hosts: freebsd
become: true
tasks:
- name: Set hostname
community.general.sysrc:
name: hostname
value: "{{ inventory_hostname }}"
- name: Enable sshd
community.general.sysrc:
name: sshd_enable
value: "YES"
- name: Set default gateway
community.general.sysrc:
name: defaultrouter
value: "203.0.113.1"
- name: Enable PF firewall
community.general.sysrc:
name: pf_enable
value: "YES"
This module understands rc.conf syntax and handles quoting, escaping, and merging correctly. Never use lineinfile to edit rc.conf -- it will eventually break your configuration.
Service Management
The built-in service module works with FreeBSD's rc.d system:
yaml
- name: Manage services
hosts: freebsd
become: true
tasks:
- name: Start and enable sshd
service:
name: sshd
state: started
enabled: true
- name: Restart networking
service:
name: netif
state: restarted
Ansible detects FreeBSD automatically and uses the correct service management commands (service(8) and sysrc(8)) behind the scenes.
FreeBSD-Specific Modules and Gotchas
Working with FreeBSD in Ansible requires understanding several key differences from Linux.
No systemd
FreeBSD uses the rc.d init system, not systemd. There are no unit files, no systemctl, no journal. Services are controlled through shell scripts in /usr/local/etc/rc.d/ (for ports/packages) and /etc/rc.d/ (for base system services). The service module handles this transparently, but if you are porting playbooks from Linux, you will need to remove any systemd-specific tasks.
rc.conf Is the Single Source of Truth
On FreeBSD, /etc/rc.conf controls which services start at boot and their configuration. Always use the sysrc module to modify it. Direct file manipulation with lineinfile or template can corrupt the file or create conflicting entries.
pkg vs Ports
The pkgng module manages binary packages installed via pkg. If you need to compile software from the ports tree with custom options, use the shell module:
yaml
- name: Build from ports with custom options
shell: |
cd /usr/ports/www/nginx && \
make OPTIONS_SET="HTTP_GZIP HTTP_SSL HTTP_V2" \
BATCH=yes install clean
args:
creates: /usr/local/sbin/nginx
The creates parameter ensures this task only runs if NGINX is not already installed, maintaining idempotency.
File Paths
FreeBSD installs third-party software under /usr/local/, not /usr/ or /opt/. Configuration files go in /usr/local/etc/, not /etc/. Account for this in all template and copy tasks:
yaml
- name: Deploy nginx configuration
template:
src: nginx.conf.j2
dest: /usr/local/etc/nginx/nginx.conf
owner: root
group: wheel
mode: '0644'
Note that the default group is wheel, not root as on Linux.
User Management
FreeBSD uses pw(8) for user management. The Ansible user module supports FreeBSD, but some parameters behave differently. The default shell is /bin/sh, not /bin/bash, and the skeleton directory is /usr/share/skel.
Practical Playbook: Hardening a FreeBSD Server
This playbook implements essential [FreeBSD hardening](/blog/hardening-freebsd-server/) measures. Save it as playbooks/harden.yml:
yaml
---
- name: Harden FreeBSD servers
hosts: freebsd
become: true
vars:
ssh_port: 22
allowed_users: "admin"
tasks:
- name: Update all packages
community.general.pkgng:
name: "*"
state: latest
- name: Install security tools
community.general.pkgng:
name:
- doas
- lynis
- rkhunter
state: present
- name: Configure doas
copy:
content: |
permit nopass keepenv :wheel
permit nopass keepenv root
dest: /usr/local/etc/doas.conf
owner: root
group: wheel
mode: '0600'
- name: Harden SSH - disable root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
notify: restart sshd
- name: Harden SSH - disable password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
notify: restart sshd
- name: Harden SSH - allow only specific users
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?AllowUsers'
line: "AllowUsers {{ allowed_users }}"
notify: restart sshd
- name: Harden SSH - disable X11 forwarding
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?X11Forwarding'
line: 'X11Forwarding no'
notify: restart sshd
- name: Set secure sysctl values
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { name: 'net.inet.tcp.blackhole', value: '2' }
- { name: 'net.inet.udp.blackhole', value: '1' }
- { name: 'security.bsd.see_other_uids', value: '0' }
- { name: 'security.bsd.see_other_gids', value: '0' }
- { name: 'security.bsd.unprivileged_read_msgbuf', value: '0' }
- { name: 'security.bsd.unprivileged_proc_debug', value: '0' }
- { name: 'kern.randompid', value: '1' }
- name: Enable PF firewall
community.general.sysrc:
name: pf_enable
value: "YES"
- name: Deploy PF firewall rules
template:
src: templates/pf.conf.j2
dest: /etc/pf.conf
owner: root
group: wheel
mode: '0600'
notify: reload pf
- name: Enable and start PF
service:
name: pf
state: started
enabled: true
- name: Set secure console permissions
copy:
content: |
console none unknown off insecure
dest: /etc/ttys.d/console.conf
owner: root
group: wheel
mode: '0644'
when: false # Enable manually after verifying physical access
- name: Clear /tmp on boot
community.general.sysrc:
name: clear_tmp_enable
value: "YES"
- name: Enable syslog TLS (if remote logging configured)
community.general.sysrc:
name: syslogd_flags
value: "-ss"
handlers:
- name: restart sshd
service:
name: sshd
state: restarted
- name: reload pf
shell: pfctl -f /etc/pf.conf
Create the PF template at templates/pf.conf.j2:
jinja2
# Ansible managed - do not edit manually
ext_if = "vtnet0"
set block-policy drop
set skip on lo0
# Scrub incoming packets
scrub in all
# Default deny
block all
# Allow outbound traffic
pass out quick on $ext_if proto { tcp, udp, icmp } from ($ext_if) to any modulate state
# Allow SSH
pass in on $ext_if proto tcp from any to ($ext_if) port {{ ssh_port }} flags S/SA modulate state
# Allow HTTP and HTTPS (webservers only)
{% if 'webservers' in group_names %}
pass in on $ext_if proto tcp from any to ($ext_if) port { 80, 443 } flags S/SA modulate state
{% endif %}
# Allow ICMP ping
pass in on $ext_if inet proto icmp icmp-type echoreq
Run the hardening playbook:
bash
ansible-playbook playbooks/harden.yml
Practical Playbook: Deploying NGINX with Let's Encrypt
This playbook installs and configures NGINX with automatic TLS certificates. For the full [NGINX setup guide on FreeBSD](/blog/nginx-freebsd-production-setup/), see our dedicated tutorial.
Save as playbooks/nginx-letsencrypt.yml:
yaml
---
- name: Deploy NGINX with Let's Encrypt on FreeBSD
hosts: webservers
become: true
vars:
domain: "example.com"
webroot: "/usr/local/www/{{ domain }}"
email: "admin@example.com"
tasks:
- name: Install NGINX and Certbot
community.general.pkgng:
name:
- nginx
- py311-certbot
- py311-certbot-nginx
state: present
- name: Create webroot directory
file:
path: "{{ webroot }}"
state: directory
owner: www
group: www
mode: '0755'
- name: Deploy initial NGINX configuration
template:
src: templates/nginx-initial.conf.j2
dest: /usr/local/etc/nginx/nginx.conf
owner: root
group: wheel
mode: '0644'
notify: reload nginx
- name: Enable and start NGINX
community.general.sysrc:
name: nginx_enable
value: "YES"
- name: Start NGINX
service:
name: nginx
state: started
- name: Obtain Let's Encrypt certificate
command: >
certbot certonly
--webroot
--webroot-path {{ webroot }}
--email {{ email }}
--agree-tos
--no-eff-email
-d {{ domain }}
-d www.{{ domain }}
args:
creates: "/usr/local/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
- name: Deploy production NGINX configuration with TLS
template:
src: templates/nginx-tls.conf.j2
dest: /usr/local/etc/nginx/nginx.conf
owner: root
group: wheel
mode: '0644'
notify: reload nginx
- name: Configure certificate auto-renewal
cron:
name: "certbot renew"
minute: "30"
hour: "3"
weekday: "1"
job: "/usr/local/bin/certbot renew --quiet --deploy-hook 'service nginx reload'"
user: root
- name: Set NGINX performance tuning in rc.conf
community.general.sysrc:
name: nginx_flags
value: ""
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
Practical Playbook: Managing FreeBSD Jails
[FreeBSD jails](/blog/freebsd-jails-guide/) are lightweight containers native to the operating system. This playbook automates jail creation and management using the bastille jail manager.
Save as playbooks/jails.yml:
yaml
---
- name: Manage FreeBSD jails with Bastille
hosts: jailhosts
become: true
vars:
jail_release: "14.2-RELEASE"
jails:
- name: webjail
ip: "10.0.0.10"
interface: "lo1"
packages:
- nginx
- php83
- name: dbjail
ip: "10.0.0.20"
interface: "lo1"
packages:
- postgresql16-server
- postgresql16-client
tasks:
- name: Install Bastille
community.general.pkgng:
name: bastille
state: present
- name: Enable Bastille at boot
community.general.sysrc:
name: bastille_enable
value: "YES"
- name: Create cloned loopback interface
community.general.sysrc:
name: cloned_interfaces
value: "lo1"
- name: Configure loopback network
community.general.sysrc:
name: ifconfig_lo1_alias0
value: "inet 10.0.0.0/24"
- name: Bring up lo1 interface
command: ifconfig lo1 create
failed_when: false
changed_when: false
- name: Configure lo1 address
command: ifconfig lo1 inet 10.0.0.0/24
failed_when: false
changed_when: false
- name: Bootstrap Bastille release
command: "bastille bootstrap {{ jail_release }} update"
args:
creates: "/usr/local/bastille/releases/{{ jail_release }}/bin/freebsd-version"
- name: Create jails
command: "bastille create {{ item.name }} {{ jail_release }} {{ item.ip }} {{ item.interface }}"
args:
creates: "/usr/local/bastille/jails/{{ item.name }}"
loop: "{{ jails }}"
- name: Install packages in each jail
command: "bastille pkg {{ item.0.name }} install -y {{ item.1 }}"
loop: "{{ jails | subelements('packages') }}"
changed_when: false
- name: Start all jails
command: "bastille start {{ item.name }}"
loop: "{{ jails }}"
changed_when: false
failed_when: false
- name: Add PF rules for jail NAT
blockinfile:
path: /etc/pf.conf
marker: "# {mark} ANSIBLE MANAGED - JAIL NAT"
block: |
nat on vtnet0 from 10.0.0.0/24 to any -> (vtnet0)
pass in on lo1 all
notify: reload pf
handlers:
- name: reload pf
shell: pfctl -f /etc/pf.conf
For a complete guide on jail networking, resource limits, and template management, see our [FreeBSD jails guide](/blog/freebsd-jails-guide/).
Roles and Galaxy Collections for FreeBSD
As your playbooks grow, organize them into reusable roles.
Creating a Role
bash
ansible-galaxy role init roles/freebsd_base
This creates the standard role directory structure. Move your common FreeBSD tasks into roles/freebsd_base/tasks/main.yml:
yaml
---
- name: Bootstrap pkg
raw: env ASSUME_ALWAYS_YES=yes pkg bootstrap
changed_when: false
- name: Update package repository
community.general.pkgng:
name: pkg
state: latest
- name: Install base packages
community.general.pkgng:
name: "{{ freebsd_base_packages }}"
state: present
- name: Set timezone
file:
src: "/usr/share/zoneinfo/{{ freebsd_timezone }}"
dest: /etc/localtime
state: link
force: true
- name: Configure NTP
community.general.sysrc:
name: ntpd_enable
value: "YES"
- name: Start NTP
service:
name: ntpd
state: started
Define defaults in roles/freebsd_base/defaults/main.yml:
yaml
---
freebsd_timezone: "UTC"
freebsd_base_packages:
- vim
- htop
- tmux
- curl
- rsync
- doas
- py311-pip
Galaxy Collections
Install the community.general collection, which contains FreeBSD-specific modules:
bash
ansible-galaxy collection install community.general
Create a requirements.yml file to pin collection versions:
yaml
---
collections:
- name: community.general
version: ">=9.0.0"
Install from the requirements file:
bash
ansible-galaxy collection install -r requirements.yml
Some community roles specifically target FreeBSD. Search Galaxy for them:
bash
ansible-galaxy search freebsd --platforms FreeBSD
Vault for Secrets Management
Never store passwords, API keys, or private data in plain text. Ansible Vault encrypts sensitive variables.
Creating Encrypted Variables
Create an encrypted variables file:
bash
ansible-vault create group_vars/freebsd/vault.yml
Add your secrets:
yaml
vault_root_password: "your-root-password-here"
vault_db_password: "database-password-here"
vault_certbot_email: "real-email@example.com"
Using Vault in Playbooks
Reference vault variables like any other variable:
yaml
- name: Set database password
shell: "echo '{{ vault_db_password }}' | pw usermod postgres -h 0"
no_log: true
The no_log: true directive prevents Ansible from printing the password in its output.
Running Playbooks with Vault
Using a password file (recommended for automation):
bash
echo 'your-vault-password' > .vault_pass
chmod 600 .vault_pass
ansible-playbook playbooks/harden.yml --vault-password-file .vault_pass
Add .vault_pass to your .gitignore immediately. Never commit vault passwords to version control.
Using interactive password prompt:
bash
ansible-playbook playbooks/harden.yml --ask-vault-pass
Testing with Molecule
Molecule provides a framework for testing Ansible roles. Since FreeBSD does not run in Docker containers natively, use Vagrant with VirtualBox or a cloud provider as the driver.
Install Molecule
bash
pip install molecule molecule-vagrant python-vagrant
Initialize Molecule for a Role
bash
cd roles/freebsd_base
molecule init scenario --driver-name vagrant
Edit molecule/default/molecule.yml:
yaml
---
dependency:
name: galaxy
driver:
name: vagrant
provider:
name: virtualbox
platforms:
- name: freebsd-test
box: freebsd/FreeBSD-14.2-RELEASE
memory: 1024
cpus: 1
provisioner:
name: ansible
inventory:
group_vars:
all:
ansible_python_interpreter: /usr/local/bin/python3.11
verifier:
name: ansible
Create verification tests in molecule/default/verify.yml:
yaml
---
- name: Verify FreeBSD base role
hosts: all
become: true
tasks:
- name: Check that base packages are installed
command: pkg info vim
changed_when: false
- name: Check NTP is running
command: service ntpd status
changed_when: false
- name: Verify timezone is set
command: readlink /etc/localtime
register: tz_result
changed_when: false
failed_when: "'UTC' not in tz_result.stdout"
Run the full test cycle:
bash
molecule test
This creates a FreeBSD VM, applies your role, runs verification tests, and destroys the VM. Use molecule converge during development to keep the VM running between iterations.
An alternative to Vagrant is testing against a [FreeBSD VPS](/blog/freebsd-vps-setup/) instance. Cloud providers like Vultr and DigitalOcean offer FreeBSD images that spin up in under a minute, making them practical for CI/CD pipelines.
Putting It All Together
A site-wide playbook that calls your roles in sequence:
yaml
---
- name: Configure all FreeBSD servers
hosts: freebsd
become: true
roles:
- freebsd_base
- freebsd_hardening
- name: Configure web servers
hosts: webservers
become: true
roles:
- freebsd_nginx
- freebsd_certbot
- name: Configure database servers
hosts: dbservers
become: true
roles:
- freebsd_postgresql
- name: Configure jail hosts
hosts: jailhosts
become: true
roles:
- freebsd_jails
Run everything:
bash
ansible-playbook site.yml --vault-password-file .vault_pass
Run only against web servers:
bash
ansible-playbook site.yml --limit webservers
Dry run to see what would change:
bash
ansible-playbook site.yml --check --diff
FAQ
Does Ansible work with FreeBSD out of the box?
Yes. Ansible supports FreeBSD as a target platform. You need Python installed on the FreeBSD host and must set ansible_python_interpreter to the correct path (/usr/local/bin/python3.11). The community.general collection includes FreeBSD-specific modules for package management and rc.conf manipulation. No custom plugins or patches are needed.
What become method should I use on FreeBSD?
FreeBSD does not include sudo in the base system. Your best options are su (requires root password, available by default) or doas (requires installing the doas package). Set ansible_become_method in your group variables. For automated environments, doas with permit nopass keepenv :wheel is the most practical choice because it does not require passing a root password.
Can Ansible manage FreeBSD packages from the ports tree?
The community.general.pkgng module manages binary packages installed via pkg. For ports, use the shell module with make install commands. Set BATCH=yes to avoid interactive prompts, and use the creates parameter to maintain idempotency. Binary packages cover the vast majority of use cases; compiling from ports is only necessary when you need custom build options.
How do I handle FreeBSD updates with Ansible?
For package updates, use community.general.pkgng with state: latest and name: "*" to update all installed packages. For base system updates (freebsd-update), use the command module:
yaml
- name: Fetch and install FreeBSD security patches
command: freebsd-update --not-running-from-cron fetch install
register: update_result
changed_when: "'Installing updates' in update_result.stdout"
Schedule this in a dedicated maintenance playbook and always test on a staging server first.
Can Ansible manage FreeBSD jails?
Yes. While Ansible does not have a dedicated jail module in the core collection, you can manage jails through jail management tools like Bastille, BastilleBSD, or iocage using the command and shell modules. The playbook example in this guide demonstrates this approach with Bastille. For complex jail environments, write a custom Ansible module or use the community-maintained jail roles on Galaxy.
How do I speed up Ansible on FreeBSD?
Enable SSH pipelining in ansible.cfg by setting pipelining = True. This reduces the number of SSH connections per task significantly. Use ControlMaster and ControlPersist in your SSH configuration to reuse connections. Consider using mitogen as a strategy plugin for further speed improvements. Finally, group independent tasks using async and poll for parallel execution within a single host.
Is Ansible suitable for managing hundreds of FreeBSD servers?
Yes. Ansible scales well with proper configuration. Use --forks to control parallelism (default is 5, increase to 20-50 for large fleets). Organize your inventory into logical groups and use --limit for targeted runs. For very large deployments, consider AWX or Ansible Automation Platform for centralized management, scheduling, and audit logging. The agentless architecture means scaling does not require deploying or updating agents across your fleet.
Conclusion
Ansible and FreeBSD are a natural fit. The agentless architecture respects FreeBSD's philosophy of keeping the base system clean. The SSH transport leverages what FreeBSD already provides. The FreeBSD-specific modules in community.general handle the platform's unique characteristics -- rc.conf management, pkgng packages, and rc.d services -- without requiring workarounds.
Start with the hardening playbook from this guide, adapt it to your environment, and build from there. Once your first playbook is working, every additional server you bring under Ansible management reduces your operational burden. Configuration drift disappears. Rebuilding a server becomes a single command. And your documentation is the playbook itself -- always up to date, always executable.