Deploying venv based Python services with Ansible and systemd
Today I deployed my first venv based python system service on my home server. This is a short post describing how I did it.
One of the hardest problems in Computer Science is naming :) In this particular case, where to put the code. Initially
I thought of something like /usr/local/lib/python<version>/site-packages/something/<app>
, but frankly only because I
have completely forgotten about /opt
, even when I was dealing with it daily no more than 3 months ago. So
/opt/<app>
it is.
Next came automating all the right steps. I install everything on that server with Ansible. It's weird to write down all the steps when one's used to do them by hand. This includes:
- Create the dir
- Copy the files
- Create the venv
- Install deps
- Install the systemd service file
The way Ansible works, you can tell it not to repeat tasks if you tell it which files it creates. The way
venvs work in python, they're created on a dir that contains the major and minor python version:
<venv_dir>/lib/python<version>
. To avoid creating the venv on every Ansible run, I need the Python version, but just
those two components. After some testing and sharing and being corrected, I got this:
python3 -c 'import sys; print(".".join(map(str, sys.version_info[0:2])))'
map(str, ...)
is needed because join()
expects a list of strings.
Another thing you can do with Ansible is to emit events that can make handlers (another type of tasks) run. In this case this is useful to restart the service if the code or the config changes, but also to refresh systemd if the service file changes.
This is the first time I used a feature of venvs. venvs contain in their bin
directory several scripts. The most
important for me so far was the bin/activate
environment file. If you source it, it will set up PATHs properly so
Python can find the deps within the venv. But it also creates a python3
and pip3
scripts that run both tools also
with the PATHs modified. This simplifies many things when manipulating venvs with Ansible, and also systemd.
The last link is on the systemd side. I didn't really want to install everything neatly in /opt
and then put the
service file outside it. systmctl link
exists exactly for that. It creates a link in /etc/systemd/system
to the service
file in /opt
. It's still not 100% clean, but at least it's managed by systemd, and you can clearly see who really
owns it. You run this command instead of systemd enable
, and it's ready for starting it.
Here's the whole play:
- name: Apache Log Exporter # this one has its own play because it's more complex than the rest hosts: servers tasks: - name: Create dirs file: path: /opt/apache-log-exporter state: directory owner: root group: root mode: 0755 tags: install, monitoring - name: Install PALE bin and conf template: src: "roles/server/files/{{ item }}" dest: "{{ item }}" owner: prometheus group: root mode: 0600 loop: - /opt/apache-log-exporter/apache-log-exporter.py - /opt/apache-log-exporter/apache-log-exporter.yaml tags: install, config, monitoring notify: - Reload Apache Log Exporter - name: Install PALE service template: src: "roles/server/files/{{ item }}" dest: "{{ item }}" owner: prometheus group: root mode: 0644 register: pale_service loop: - /opt/apache-log-exporter/apache-log-exporter.service tags: install, config, monitoring - name: Get Python Version # thanks @diji@mastodon.social command: python3 -c 'import sys; print(".".join(map(str, sys.version_info[0:2])))' register: python3_version tags: install, monitoring - name: Create PALE VEnv command: python3 -m venv --prompt pale --system-site-packages venv args: chdir: /opt/apache-log-exporter creates: "/opt/apache-log-exporter/venv/lib/python{{ python3_version.stdout_lines[0] }}" tags: install, monitoring - name: Install PALE Deps command: "/opt/apache-log-exporter/venv/bin/pip3 install {{ item }}" args: creates: "/opt/apache-log-exporter/venv/lib/python{{ python3_version.stdout_lines[0] }}/site-packages/{{ item }}" loop: - apachelogs - prometheus_client tags: install, monitoring - name: Install PALE service command: systemctl link /opt/apache-log-exporter/apache-log-exporter.service when: pale_service.changed tags: install, monitoring # systemctl link already tells systemd about the new file, but we need to restart the service # notify: # - Reload Systemd handlers: - name: Reload Apache Log Exporter service: name: apache-log-exporter state: restarted - name: Reload Systemd command: systemctl daemon-reload
The systemd unit file also uses the venv scripts:
[Unit] Description=Apache-Log-Exporter Wants=network-online.target After=network-online.target [Service] Type=simple User=prometheus Group=prometheus ExecStart=/opt/apache-log-exporter/venv/bin/python3 -u /opt/apache-log-exporter/apache-log-exporter.py --config-file /opt/apache-log-exporter/apache-log-exporter.yaml Restart=always RestartSec=10s NotifyAccess=all [Install] WantedBy=multi-user.target