Best way to always run ansible inside a virtualenv on remote machines?
Categories:
Ensuring Ansible Runs in a Virtual Environment on Remote Hosts

Discover robust strategies to consistently execute Ansible playbooks within isolated Python virtual environments on your remote machines, enhancing dependency management and system stability.
Running Ansible playbooks on remote machines often involves managing Python dependencies. To prevent conflicts with system-wide Python installations and ensure consistent execution, it's best practice to run Ansible tasks within a dedicated Python virtual environment (virtualenv). This article explores several effective methods to achieve this, ranging from simple shell
commands to more sophisticated Ansible modules and custom plugins.
Why Use Virtual Environments for Ansible?
Virtual environments provide isolated Python installations, allowing you to manage project-specific dependencies without interfering with other projects or the system's Python. For Ansible, this means:
- Dependency Isolation: Avoids conflicts between Ansible's Python module requirements and other applications on the remote host.
- Reproducibility: Ensures that playbooks run with the exact same Python packages and versions every time, regardless of the remote host's global Python setup.
- Cleanliness: Keeps the remote system's global Python environment pristine.
- Version Control: Easily switch between different Python versions or dependency sets for different playbooks or environments.
flowchart TD A[Start Ansible Playbook] --> B{Remote Host Connection} B --> C{Virtualenv Exists?} C -- No --> D[Create Virtualenv] C -- Yes --> E[Activate Virtualenv] D --> E E --> F[Install Dependencies (pip)] F --> G[Execute Ansible Tasks] G --> H[Deactivate Virtualenv (Optional)] H --> I[End Playbook]
Flowchart of Ansible execution within a remote virtual environment.
Method 1: Using shell
or command
Modules with Activation
The most straightforward way to run commands within a virtual environment is to activate it before executing your desired commands. This can be done using Ansible's shell
or command
modules. This method is simple but can become verbose for multiple commands.
---
- name: Run tasks in a virtualenv using shell
hosts: remote_hosts
vars:
venv_path: /opt/my_ansible_venv
python_version: python3.9
tasks:
- name: Ensure virtualenv is present
ansible.builtin.pip:
name: virtualenv
executable: '{{ python_version }}'
state: present
- name: Create virtualenv if it doesn't exist
ansible.builtin.command: '{{ python_version }} -m venv {{ venv_path }}'
args:
creates: '{{ venv_path }}/bin/activate'
- name: Install dependencies in virtualenv
ansible.builtin.pip:
requirements: /path/to/requirements.txt
virtualenv: '{{ venv_path }}'
virtualenv_command: '{{ python_version }} -m venv'
- name: Run a command inside the virtualenv
ansible.builtin.shell: |
source {{ venv_path }}/bin/activate
python my_script.py arg1 arg2
args:
chdir: /path/to/script/directory
register: script_output
- name: Debug script output
ansible.builtin.debug:
var: script_output.stdout
Ansible playbook demonstrating virtualenv activation and command execution using shell
.
shell
, remember that source
is a shell built-in and not an executable. It's crucial to combine the source
command with your actual command using &&
or newlines within a single shell
task to ensure they run in the same shell session.Method 2: Using the ansible.builtin.pip
Module with virtualenv
Parameter
For installing Python packages, the ansible.builtin.pip
module is the most robust and idiomatic Ansible way. It has a virtualenv
parameter that automatically handles the creation and activation of a virtual environment for package installation.
---
- name: Install Python packages into a virtualenv
hosts: remote_hosts
vars:
venv_path: /opt/my_app_venv
python_executable: /usr/bin/python3
tasks:
- name: Install virtualenv package globally if not present
ansible.builtin.pip:
name: virtualenv
executable: '{{ python_executable }}'
state: present
- name: Install application dependencies into the virtualenv
ansible.builtin.pip:
requirements: /path/to/app/requirements.txt
virtualenv: '{{ venv_path }}'
virtualenv_command: '{{ python_executable }} -m venv'
state: present
- name: Ensure virtualenv is created with specific Python version
ansible.builtin.command: '{{ python_executable }} -m venv {{ venv_path }}'
args:
creates: '{{ venv_path }}/bin/activate'
- name: Run a command using the virtualenv's python interpreter
ansible.builtin.command: '{{ venv_path }}/bin/python /path/to/app/main.py'
register: app_output
- name: Display application output
ansible.builtin.debug:
var: app_output.stdout
Using ansible.builtin.pip
to manage virtual environments and install dependencies.
ansible.builtin.pip
handles virtualenv creation for package installation, for executing arbitrary Python scripts or commands within that virtualenv, you'll still need to explicitly call the virtualenv's Python interpreter (e.g., {{ venv_path }}/bin/python
) or activate it using shell
.Method 3: Using ansible_python_interpreter
for Playbook-Wide Virtualenv
For scenarios where you want all Python-related tasks (like ansible.builtin.pip
, ansible.builtin.template
, or custom Python modules) within a playbook or host group to use a specific virtual environment, you can set the ansible_python_interpreter
inventory variable. This tells Ansible to use the Python executable located within your virtual environment.
# inventory.ini
[webservers]
server1.example.com ansible_host=192.168.1.100 ansible_python_interpreter=/opt/my_app_venv/bin/python
server2.example.com ansible_host=192.168.1.101 ansible_python_interpreter=/opt/my_app_venv/bin/python
[all:vars]
virtualenv_base_path = /opt/ansible_venvs
# Or in a playbook:
---
- name: Use specific virtualenv for all python tasks
hosts: remote_hosts
vars:
app_venv_path: /opt/my_app_venv
tasks:
- name: Ensure virtualenv is created (if not already)
ansible.builtin.command: "/usr/bin/python3 -m venv {{ app_venv_path }}"
args:
creates: "{{ app_venv_path }}/bin/activate"
delegate_to: localhost # Run this locally if venv needs to be created before interpreter is set
run_once: true
- name: Set ansible_python_interpreter for subsequent tasks
ansible.builtin.set_fact:
ansible_python_interpreter: "{{ app_venv_path }}/bin/python"
- name: Install dependencies using the virtualenv's pip
ansible.builtin.pip:
name: django
state: present
- name: Run a python script (will use the virtualenv's python)
ansible.builtin.command: "python -c 'import sys; print(sys.executable)'"
register: python_path
- name: Verify python interpreter path
ansible.builtin.debug:
var: python_path.stdout
Setting ansible_python_interpreter
in inventory or dynamically within a playbook.
ansible_python_interpreter
, ensure the virtual environment already exists on the remote host before Ansible attempts to use it. You might need a separate task to create the virtualenv first, potentially using delegate_to: localhost
if the creation process itself needs a specific Python.Choosing the Right Approach
The best method depends on your specific needs:
shell
/command
with activation: Good for one-off commands or simple scripts where you need full control over the shell environment. Can be verbose for many commands.ansible.builtin.pip
withvirtualenv
: Ideal for managing Python package dependencies within a virtual environment. It's declarative and idempotent for package management.ansible_python_interpreter
: Best when you want all Python-related Ansible modules and custom Python scripts to consistently use a specific virtual environment for a host or group. Requires the virtualenv to be pre-existing or created in an earlier step.
By consistently using virtual environments, you can significantly improve the reliability and maintainability of your Ansible deployments, ensuring that your automation runs smoothly across diverse remote environments.