Ansible Collection of roles, playbooks & plugins for Linux-based cloud infrastructure. Covers OS hardening, MariaDB, Icinga2, Nextcloud, FreeIPA, KVM & more. Bitwarden & Cloud integration. • made by Linuxfabrik
LFOps is a comprehensive Ansible Collection providing 145+ playbooks and 160+ roles to bootstrap and manage Linux-based IT infrastructures. It covers the full server lifecycle -- from initial provisioning and hardening to application deployment, monitoring, and automated backups. LFOps supports RHEL 8/9/10, Debian, and Ubuntu.
- Requirements
- Installation
- Mitogen
- Using ansible-navigator
- Usage
- Configuration
- Documentation
- Compatibility
- Tips, Tricks & Troubleshooting
- Contributing
- License
- Ansible: ansible-core >= 2.16
- Python on the controller: >= 3.10 (ansible-core 2.16/2.17) or >= 3.11 (ansible-core 2.18)
- Python on managed nodes: >= 3.6
- Dependencies: The collection depends on several community collections (see
requirements.yml). These are installed automatically when usingansible-galaxy.
Which ansible-core version should I use?
| ansible-core | Controller Python | Managed Node Python | RHEL 8 (Python 3.6) |
|---|---|---|---|
| 2.16 | 3.10 -- 3.12 | 3.6 -- 3.12 | works out of the box |
| 2.17 | 3.10 -- 3.12 | 3.7 -- 3.12 | needs Python >= 3.7 |
| 2.18 | 3.11 -- 3.14 | 3.8 -- 3.13 | needs Python >= 3.8 |
If you manage RHEL 8 hosts with the default system Python (3.6), use ansible-core 2.16. If all your managed nodes have Python >= 3.8 (e.g. RHEL 9+, Debian 12+, Ubuntu 22.04+), you can use ansible-core 2.18 for the latest features.
# Install the collection and its dependencies
ansible-galaxy collection install linuxfabrik.lfops
# Alternatively, install from Git directly
ansible-galaxy collection install git+https://github.com/Linuxfabrik/lfops.gitTo install the dependencies separately:
ansible-galaxy collection install -r requirements.ymlIf you want to contribute or test local changes, clone the repository and symlink it into Ansible's collection path:
git clone git@github.com:Linuxfabrik/lfops.git /path/to/lfops
mkdir -p ~/.ansible/collections/ansible_collections/linuxfabrik
ln -s /path/to/lfops ~/.ansible/collections/ansible_collections/linuxfabrik/lfopsImportant: The symlink target must be named lfops and point to the cloned repository root (which contains the galaxy.yml).
To verify the collection is detected:
ansible-galaxy collection list | grep linuxfabrikMitogen is an optional but recommended performance optimization for Ansible. It replaces the default SSH-based task execution with a persistent Python-to-Python channel, significantly reducing connection overhead and speeding up playbook runs.
Install Mitogen into your Ansible virtual environment:
pip install mitogenDetermine the strategy plugins path (this depends on the Python version in your venv):
python3 -c "import os, ansible_mitogen; print(os.path.join(ansible_mitogen.__path__[0], 'plugins', 'strategy'))"Add the output to your ansible.cfg:
[defaults]
strategy_plugins = /path/to/venv/lib/pythonX.Y/site-packages/ansible_mitogen/plugins/strategy
strategy = mitogen_linearSince the strategy_plugins path contains the Python version, it differs per venv. If you use multiple venvs (e.g. ansible-core 2.16 and 2.18), either use separate ansible.cfg files or install Mitogen to a shared location:
pip install --target=/opt/mitogen mitogen[defaults]
strategy_plugins = /opt/mitogen/ansible_mitogen/plugins/strategy
strategy = mitogen_linearImportant: Do not set ANSIBLE_STRATEGY=mitogen_linear via shell aliases or environment variables. This is hard to debug and overrides the config file silently. Always configure Mitogen in ansible.cfg.
The LFOps Execution Environment container image already includes Mitogen with the strategy plugins available at /opt/mitogen/ansible_mitogen/plugins/strategy. To enable it, add the following to the ansible.cfg used by ansible-navigator:
[defaults]
strategy_plugins = /opt/mitogen/ansible_mitogen/plugins/strategy
strategy = mitogen_linear| ansible-core | Mitogen | Controller Python |
|---|---|---|
| 2.16 | >= 0.3.7 | 3.10 -- 3.14 |
| 2.17 | >= 0.3.7 | 3.10 -- 3.14 |
| 2.18 | >= 0.3.7 | 3.11 -- 3.14 |
Always keep Mitogen up to date (pip install --upgrade mitogen). If you encounter SyntaxError: future feature annotations is not defined, either the Mitogen version is outdated or the Python version on the remote host is too old (< 3.7).
LFOps provides a pre-built Execution Environment (EE) container image that includes all dependencies:
pip install ansible-navigatorConfigure ansible-navigator.yml in your project directory:
ansible-navigator:
execution-environment:
container-engine: podman # or docker
enabled: true
image: ghcr.io/linuxfabrik/lfops_ee:latest
pull:
policy: missing
mode: stdoutThen run playbooks using ansible-navigator instead of ansible-playbook:
ansible-navigator run linuxfabrik.lfops.setup_basic \
--inventory path/to/inventory \
--limit myhostThe EE is based on Rocky Linux 9 with ansible-core 2.16, Python 3.11, and includes Mitogen for accelerated execution. See execution-environment.yml for the full build definition. Public container images are available on GitHub Container Registry.
Each playbook requires the target host to be in a specific inventory group named lfops_<playbook_name>. For example, to run the php playbook against myhost:
- Add the host to the
lfops_phpgroup in your inventory. - Run the playbook:
ansible-playbook linuxfabrik.lfops.php \
--inventory path/to/inventory \
--limit myhostUse --diff to see changes, --check for dry-run mode, and --tags to run specific parts of a playbook.
Add your host to the required groups in the inventory:
[lfops_hetzner_vm]
myhost
[lfops_setup_basic]
myhost
[lfops_monitoring_plugins]
myhost
[lfops_setup_nextcloud]
myhostThen run the playbooks in order:
# Provision the VM
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.hetzner_vm --limit myhost
# Basic setup (hardening, SSH, firewall, chrony, etc.)
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.setup_basic --limit myhost
# Deploy monitoring
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.monitoring_plugins --limit myhost
# Deploy a full application stack (playbooks prefixed by "setup_" include all dependencies)
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.setup_nextcloud --limit myhost
# Change specific settings afterwards, e.g. PHP
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.setup_nextcloud --limit myhost --tags phpThe linuxfabrik.lfops.all playbook imports all other playbooks. This is useful when a change affects multiple playbooks. For example, to deploy an updated MariaDB dump script to all hosts that have MariaDB (whether installed directly or as part of WordPress, Nextcloud, etc.):
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.all --tags mariadb_server --limit myhostAlways use --tags and --limit with the all playbook.
The playbooks support skipping individual roles using inventory variables. For example, to skip the firewall role in setup_basic:
setup_basic__skip_firewall: trueIn playbooks that support role injections (like setup_icinga2_master), there are two variables:
playbook_name__role_name__skip_role: Skips the role and disables the role's injections. Have a look at the playbook for the default value.playbook_name__role_name__skip_role_injections: Disables or re-enables the role's injections. Takes priority overplaybook_name__role_name__skip_role. Defaults toplaybook_name__role_name__skip_rolefor ease of use. Have a look at the playbook for the affected injections.
Place an ansible.cfg in your inventory/project directory:
[defaults]
forks = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = ~/.ansible_cache
fact_caching_timeout = 86400
host_key_checking = False
inventory_ignore_extensions = ~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo, .csv, .md
inventory_ignore_patterns = '(host|group)_files'
nocows = 1
retry_files_enabled = False
strategy_plugins = /path/to/ansible_mitogen/plugins/strategy ; see "Mitogen" section
strategy = mitogen_linear
timeout = 15
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60sIt would sometimes be convenient to allow the user to set a default for multiple roles. However, since we strictly prefix all our variables with the role name, this is not that straightforward. Instead, we provide a handful of variables prefixed with lfops__ that act as the default for multiple roles. It is, of course, still possible to overwrite the LFOps-wide variable with the role-specific one (for example, the value of icingaweb2_module_director__monitoring_plugins_version takes precedence over that of lfops__monitoring_plugins_version).
This variable is used as the default whenever the version of the Linuxfabrik Monitoring Plugins is required. For example, it is used to deploy the correct version of the Director Basket and Grafana Dashboards in the icingaweb2_module_director and icingaweb2_module_grafana roles, respectively. For documentation of the value, have a look at the monitoring_plugins__version variable in the monitoring_plugins role README.
lfops__monitoring_plugins_version: 'dev'This variable aims to simplify the management of rpmnew and rpmsave files (and their Debian equivalents) by allowing the admin to remove them with LFOps. The workflow would be to adjust the template in LFOps according to the new config file, then deploy with --extra-vars='lfops__remove_rpmnew_rpmsave=true' to update the config and remove the rpmnew / rpmsave in one run.
This variable is used as the default across all repo_* roles if it is set. Can be used to authenticate against the repository server using HTTP basic auth. Have a look at the respective role's README for details.
Note: Currently this only works for RPM repositories.
lfops__repo_basic_auth_login:
username: 'mirror-user'
password: 'linuxfabrik'This variable is used as the default across all repo_* roles if it is set. Can be used to set the URL to a custom mirror server providing the repository. Have a look at the respective role's README for details.
lfops__repo_mirror_url: 'https://mirror.example.com'LFOps includes a Bitwarden lookup plugin that fetches secrets from your vault (and creates items if they do not exist). Requires the bw CLI version v2022.9.0+.
Usage example in your inventory:
grafana_grizzly__grafana_service_account_login:
"{{ lookup('linuxfabrik.lfops.bitwarden_item',
{
'hostname': inventory_hostname,
'purpose': 'Grafana Service Account Token',
'username': 'grizzly',
'collection_id': lfops__bitwarden_collection_id,
'organization_id': lfops__bitwarden_organization_id,
},
) }}"Before running Ansible, unlock your vault and start the Bitwarden API server:
export BW_SESSION="$(bw unlock --raw)"
bw status | jq
bw serve --hostname 127.0.0.1 --port 8087 &The lookup returns multiple keys, including username and password. To get only the password:
freeipa_server__directory_manager_password:
"{{ lookup('linuxfabrik.lfops.bitwarden_item',
{
'hostname': inventory_hostname,
'purpose': 'FreeIPA',
'username': 'cn=Directory Manager',
'collection_id': lfops__bitwarden_collection_id,
'organization_id': lfops__bitwarden_organization_id,
},
)['password'] }}"Note: When using the lookup in group_vars, avoid inventory_hostname if you want a shared credential (e.g. one password for all FreeIPA clients). Instead, use the actual server hostname:
freeipa_server__ipa_admin_password:
"{{ lookup('linuxfabrik.lfops.bitwarden_item',
{
'hostname': 'freeipa.example.com',
'purpose': 'FreeIPA',
'username': 'admin',
'collection_id': lfops__bitwarden_collection_id,
'organization_id': lfops__bitwarden_organization_id,
},
)['password'] }}"See ansible-doc -t lookup linuxfabrik.lfops.bitwarden_item for all options.
- Ansible Roles: Each role has its own README file in
roles/<role_name>/. - Ansible Plugins: Available through
ansible-doc. For example:ansible-doc linuxfabrik.lfops.gpg_key. - Changelog: CHANGELOG.md
- Compatibility: COMPATIBILITY.md
- Contributing: CONTRIBUTING.rst
- Issue Tracker: GitHub Issues
Which Ansible role is proven to run on which OS? See COMPATIBILITY.md.
Q: Where should I set ansible_become: true?
Don't use become: true in role playbooks. Instead, set ansible_become: true in your group_vars or host_vars only (not in all.yml -- localhost must not be part of the group, otherwise you'll get errors like sudo: a password is required).
Q: How do I find all groups a host belongs to?
ansible --inventory path/to/inventory myhost -m debug -a "var=group_names"Q: How do I connect as an unprivileged user?
Make sure the user is allowed to switch to all other accounts, not just root. Otherwise tasks using become_user: 'apache' etc. will fail. The sudoers entry must use (ALL):
ansible-user ALL=(ALL) NOPASSWD: ALL
Q: How do I find out which playbooks ran against a host?
All playbooks log every run to /var/log/linuxfabrik-lfops.log on the target host:
2024-05-23 11:15:26.604794 - Playbook linuxfabrik.lfops.apps: START
2024-05-23 11:15:32.877064 - Playbook linuxfabrik.lfops.apps: END
Q: Debian: No package matching '...' is available
Run apt update before running the specific role.
Q: [WARNING]: Collection x.y does not support Ansible version 2.16.xx
Install a newer Ansible version and update all collections:
python3 -m venv ~/venvs/ansible-2.18
source ~/venvs/ansible-2.18/bin/activate
pip install --upgrade pip
pip install 'ansible-core~=2.18.0'
ansible-galaxy collection install -r requirements.yml --upgradeQ: error creating bridge interface ...: Numerical result out of range
On Linux an interface name must not exceed 15 characters. Choose a shorter bridge name.
See CONTRIBUTING.rst for guidelines.
This project is licensed under the Unlicense.