Ansible Meets UV

1 hour ago 2

Introduction

Ansible is still the most powerful Configuration Management tool in 2025. It’s used mainly to configure and manage bare-metal servers and virtual machines.
But can also be used to for network automation, cloud resource provisioning, CICD pipelines, etc.

Ansible’s popularity (and configuration management tool’s in general) has decreased in recent years, probably due to a shift towards immutable infrastructure (despite that Ansible could be used for this too).
But there are still new applications in which Ansible could shine.

So why bother with Ansible in 2025? Because it gives you interesting features out of the box:1) Idempotency, 2) Drift detection, 3) a really high level ‘language’. And because used with UV, Ansible can be used as a standalone ‘scripting’ ‘language’, with idempotency and drift detection, again out of the box. For certain tasks, those are killer features. Which tasks? Think for example installation of software; installation of tools; Developer Experience (DevEx) setup; CD part of a CICD pipeline; setup of CI ephemeral runners etc.

Why hasn’t it been done before?

But historically Ansible, and more specifically ansible-playbooks, haven’t been thought to be used as a standalone script, why?

  1. Because installing Ansible in itself is (was) complicated.
    1.1. Because being written in python, Ansible suffers python’s known packaging issues.
    1.2. Because, Ansible versioning has been historically complicated.
    1.3. Because probably, neither the author of the playbook nor the final user are python experts to be able to debug the packaging issues.
  2. Because Ansible has been used already in a repo with complex inventory structures, complex overrides, with roles that need to be installed. And despite that this is normal, the usual complexity might shy away the usability or applicability of standalone script.
  3. Because scripting could be done with simpler and more powerful tools like Bash and Make. Make can give us idempotency, but not drift detection out of the box. Bash can’t give neither idempotency nor drift detection, you’ll need to code them yourself.
    The same for python or other scripting language, but they don’t provide a high-level language. At least not high-enough to Bash, Make or Ansible.
  4. Ansible is too verbose. Probably 3 lines of Bash can do what 50 lines of ansible-playbook does. Yet, if we want idempotency, drift detection and readability we would need to write some extra code.

(Just to be clear, I’m not advocating replacing Bash or Make with Ansible, that would be insane. What I’m trying to say, is that for certain specific set of tasks, Ansible could be a better fit. What you’ll see later is that I actually use together Make, Bash and Ansible)

The UV solution and example

Back to the point, The UV project, has solved one of Ansible’s major issues: the difficulty to install, use and share. You can read about UV here: https://docs.astral.sh/uv/

Look at the ansible-playbook below. What is important is the shebang line: it is an executable script. It is a ‘UV run tool’ script (to learn more see https://docs.astral.sh/uv/concepts/tools/#relationship-to-uv-run), though in reality it’s an Ansible playbook.
The only dependency of this script is UV, no need to have python, pip or Ansible installed.
UV installs python and Ansible for us, in an invisible manner. And now, with some caveats, I can run this script almost anywhere, the script could be distributed. And no one needs to know that it even is an ansible playbook!

Below is a script that I carved out from a larger project. Don’t expect to run it as is, its purpose is to exemplify what can be done with Ansible and UV.

#!/usr/bin/env -S uv tool run --with "ansible==12.0.0" --from [email protected] ansible-playbook -D -K -v # # Usage: # - Assuming this file is called 'install', and you had already chmod +x install # ./install # ./install -CD # For dry-run --- - hosts: localhost tasks: - name: Update apt cache become: true ansible.builtin.apt: update_cache: true cache_valid_time: 21600 - name: Check if running in a Docker container ansible.builtin.stat: path: /.dockerenv register: in_docker - name: Install OS dependencies become: true ansible.builtin.apt: name: - curl - "{{ 'libglib2.0-0t64' if (ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('24.04', '>=') or ansible_distribution == 'Debian' and ansible_distribution_version is version('13', '>=')) else 'libglib2.0-0' }}" update_cache: false state: present - block: - name: Install python dependencies become: false shell: | uv venv --python 3.12.0 --relocatable uv sync --frozen --no-install-package args: creates: .venv rescue: - name: Cleanup .venv ansible.builtin.file: path: .venv state: absent - ansible.builtin.fail: msg: Error while creating the virtual environment - name: Ensure MariaDB server is enabled and running become: true ansible.builtin.service: name: mariadb state: started enabled: true when: - not in_docker.stat.exists - not lookup('ansible.builtin.env', 'GITHUB_ACTIONS') == "true" - name: Ensure MariaDB server is enabled and running (docker) become: true shell: | nohup mysqld_safe --user=mysql --datadir=/var/lib/mysql > /var/log/mysqld.log 2>&1 & when: in_docker.stat.exists or lookup('ansible.builtin.env', 'GITHUB_ACTIONS') == "true" - name: Create my_project root user with privileges become: true community.mysql.mysql_user: name: my_project_root_user password: my_project_root_password host: localhost priv: '*.*:ALL,GRANT' state: present login_unix_socket: /var/run/mysqld/mysqld.sock environment: PYTHONPATH: /usr/lib/python3/dist-packages/ - name: Render /etc/my_conf/project.conf config file become: true ansible.builtin.copy: dest: /etc/my_conf/project.conf content: | (...) [api] path = {{ playbook_dir }}/.venv/lib/python3.12/site-packages/modules/ (...) - name: Check if database already initialized become: true shell: | mysql -e "SHOW DATABASES" | grep target_database_to_check register: target_database_to_check_is_present ignore_errors: true changed_when: false failed_when: false - name: Long and destructive database initialization script shell: | uv run python3 install_databases.py args: executable: /bin/bash when: target_database_to_check_is_present.rc != 0

What we get for free

We get:

  1. Nice looking interface in the terminal, with colors and the possibility of extra verbosity.
  2. Idempotency: we can run it n times and we will get the same result. No fear of side effects.
    In fact, if not necessary the tasks won’t be executed at all.
  3. Drift detection: we can run the script in dry-run mode giving the -CD flag.
    And even if you don’t run it in dry-run the script will output the drift.

Looking specifically to the playbook, we get the following for free:

  1. Update cache only if necessary! Makes subsequent runs faster.
  2. Easy differentiation between which tasks needs to run as root and which doesn’t. With “become” keyword. And single sudo password prompt at the beginning.
  3. Easy differentiate between running in a container or a runner. Checking for /.dockerenv or GITHUB_ACTIONS env var. (This is not ansible specific).
  4. Easy differentiation between OS and distribution. Easy to distinguish between an Ubuntu machine of certain version, from Debian, or from other distros.
  5. Install python dependencies only if .venv does not exist already. (This is easier in Make, and easy to do in Bash).
  6. Delete .venv in case the complete operation did not succeed. (Easier in Make, more code in Bash)
  7. Ensure DB (and other services) are running, even without systemd.
  8. Easy provisioning of database users, with determined permissions. (This involves code in Make or Bash).
  9. Easy rendering of configuration files, and check drift. (Harder in Make and Bash)

Conclusion

As a conclusion, UV has given a new wind to Ansible. Ansible is easier to install than ever, and this opens the possibilities to use its powers in new and unusual ways.

Read Entire Article