Ansible ile Python Test Ortamlarını Hazırlamak

Selamlar, bu yazıda biraz can sıkıcı ama bir o kadar da hayat kurtaran bir konuya bakacağız: birden fazla test sunucusunda Python ortamını tutarlı tutmak. Tek makinede python -m venv yetiyor, evet. Ama üç-beş runner, bir de staging kutusu eklendiğinde 'benim makinemde geçiyordu' tiyatrosu başlıyor. Ansible burada gerçekten işin tonunu değiştiriyor, hadi bir bakalım.

Neden el ile yapmamak gerekiyor?

Açıkçası ilk başlarda ben de 'küçük bir bash script yeter' diye düşünmüştüm. İki sunucuda bile bu inanç çatlıyor: birinde python3.11, diğerinde python3.10, üçüncüsünde libffi-dev eksik, sonra bir kütüphane kaynaktan derlenmeye çalışıyor ve siz logların başında bir saat geçiriyorsunuz. Ansible'ın güzel tarafı, her görevin idempotent olması: aynı playbook'u on kez çalıştırın, yine aynı sonuca varır. Yani 'acaba bozar mıyım' korkusu olmadan koşturursunuz.

Envanter ile başlayalım

Önce hangi makinelere dokunacağımızı netleştirmemiz lazım. Basit bir inventory/hosts dosyası işi görüyor:

[test_runners]
runner01 ansible_host=10.0.2.21
runner02 ansible_host=10.0.2.22

[test_runners:vars]
ansible_user=ci
ansible_python_interpreter=/usr/bin/python3

Buradaki ansible_python_interpreter küçük ama önemli bir ayrıntı. Hedef sistemde Python 2 ile 3 birlikteyse Ansible hangisini kullanacağını şaşırabiliyor; biz açık açık söyleyince mesele kapanıyor.

Sistem bağımlılıkları ve venv

Test ortamının temeli iki parçadan oluşuyor: sistem paketleri (derleme için gerekenler) ve proje seviyesinde bir virtualenv. Bunları tek bir playbook'ta toplayalım:

---
- name: Python test ortamini hazirla
  hosts: test_runners
  become: true

  vars:
    proje_adi: <Proj>
    proje_dizini: /opt/{{ proje_adi }}
    proje_kullanici: ci
    venv_dizini: /opt/{{ proje_adi }}/venv

  tasks:
    - name: Sistem paketlerini kur
      ansible.builtin.package:
        name:
          - python3
          - python3-venv
          - python3-dev
          - build-essential
          - libssl-dev
          - libffi-dev
          - git
        state: present

    - name: Proje dizinini olustur
      ansible.builtin.file:
        path: '{{ proje_dizini }}'
        state: directory
        owner: '{{ proje_kullanici }}'
        group: '{{ proje_kullanici }}'
        mode: '0755'

    - name: Venv olustur ve pip'i guncelle
      ansible.builtin.pip:
        virtualenv: '{{ venv_dizini }}'
        virtualenv_command: python3 -m venv
        name: pip
        state: latest
      become_user: '{{ proje_kullanici }}'

    - name: Test bagimliliklarini kur
      ansible.builtin.pip:
        virtualenv: '{{ venv_dizini }}'
        requirements: '{{ proje_dizini }}/requirements-test.txt'
      become_user: '{{ proje_kullanici }}'

Burada dikkat çekmek istediğim nokta become_user kullanımı. Venv'i root ile oluşturursanız sonradan CI kullanıcısı write izni alamaz ve pip install patlar. Ben bu hatayı bir kez yapmıştım, sonra bir daha yapmadım.

pytest ve coverage'ı işin içine sokmak

Test ortamı sadece 'paketleri kurduk' demek değil; çalıştırma komutunu da standartlaştırmalıyız. requirements-test.txt dosyasının içine en azından şunlar girer:

pytest>=8.0
pytest-cov>=5.0
pytest-xdist>=3.5
coverage[toml]>=7.4

Sonra projenin pyproject.toml dosyasına pytest'in nereye bakacağını yazıyoruz:

- name: pyproject.toml icin pytest ayari yerlestir
  ansible.builtin.blockinfile:
    path: '{{ proje_dizini }}/pyproject.toml'
    marker: '# {mark} ANSIBLE PYTEST AYARI'
    block: |
      [tool.pytest.ini_options]
      addopts = '-ra -q --cov=src --cov-report=xml --cov-report=term'
      testpaths = ['tests']

blockinfile modülü, dosyayı tamamen ezmeden sadece kendi bloğunu yönetiyor. Yani projenizdeki diğer ayarlar yerinde kalıyor. Kanaatim odur ki üretim playbook'larında template yerine blockinfile/lineinfile kullanmak, başkalarının yaptığı el değişikliklerine biraz nezaket göstermek demek.

CI tarafına bağlamak

Playbook'u CI'da çalıştırmak için karmaşık bir orkestrasyona girmeye gerek yok. GitHub Actions örneği:

- name: Test ortamini hazirla
  run: |
    ansible-playbook -i inventory/hosts playbook.yml \
      --limit test_runners \
      --extra-vars 'proje_adi={{ github.event.repository.name }}'

- name: Testleri kosturlar uzerinde calistir
  run: |
    ansible test_runners -i inventory/hosts -m shell \
      -a 'cd /opt/{{ proje_adi }} && venv/bin/pytest -n auto'

Önce --check --diff ile bir provadan geçirmenizi öneriyorum. Hiçbir değişiklik yapmadan size 'şunu şuna çevirecektim' diye söylüyor; production öncesi paha biçilmez bir alışkanlık.

Sık karşılaşılan tuzaklar

  • Venv'i root ile oluşturmak: Sonra CI kullanıcısı pip install çalıştıramaz. become_user her zaman ayar.
  • Sistem Python'una pip install yapmak: --break-system-packages ile zorlamayın; venv kullanın, distro'nun python'unu temiz bırakın.
  • pip görevini her seferinde state: latest ile çağırmak: requirements dosyası varsa ona güvenin, sürümleri orada sabitleyin. Aksi halde test sonuçları rastgele kütüphane güncellemeleri yüzünden değişir ve nedenini bulamazsınız.
  • gather_facts: false ile başlamak: Hız için cazip görünüyor ama paket yöneticisi modülü dağıtım bilgisini bekliyor; package yerine apt veya dnf yazmaya başlarsanız taşınabilirlik gider.

Doğrulama

Çalışıp çalışmadığını anlamanın en hızlı yolu:

ansible test_runners -i inventory/hosts -m shell \
  -a 'venv/bin/python -c "import pytest, coverage; print(pytest.__version__, coverage.__version__)"'

Tüm runner'lardan aynı sürümü görüyorsanız iş tamam.

Kapanış

Bu yazıda Ansible ile Python test ortamlarını kurmanın iskeletine baktık; envanter, venv, pytest ayarı ve CI bağlantısı. Bence bu kadar otomatize edilmiş bir akış kurulduktan sonra geri dönüp el ile sunucu hazırlamak istemezsiniz, ben de istemiyorum. Umarım faydalı olmuştur, bir sonraki yazıda görüşmek üzere.