На хабре уже обсуждалась тема создания виртуальных машин vagrant c комментариями "зачем мне это нужно?". В своей статье я хочу ответить на этот вопрос. Когда у вас есть проект, и в процессе его реализации к вашей команде присоединяются новые сотрудники, возникает проблема отсутствия у них идентичных условий для запуска и тестирования проекта на своих машинах. И тут вам в помощь приходит виртуальная машина Vagrant. На примере django-проекта, созданного в моей статье по etl, я хочу показать, как каждый новый участник вашего проекта за четыре консольные команды развернёт у себя среду разработки со всеми зависимостями. Это позволит в кратчайшие сроки приступить к процессу работы.

В качестве альтернативы виртуальной машине можно использовать docker-контейнеры. Но есть нюанс, что для работы с контейнерами требуется соответствующая квалификация.

Итак, представим, что у нас есть джанго-проект, который мы хотим развивать в команде.

Мы создадим виртуальную машину для дальнейшей разработки с предустановленным pyenv для управления версиями питона и poetry для управления зависимостями.Также настроим интерпретатор в IDE PyCharm Pro и VS Code.

Все исходники из данной статьи выложены в репозиторий.

Устанавливаем ПО

На хостовой (основной) операционной системе каждого участника проекта должны быть:

Обозначим структуру проекта

Маленький django-проект находится в папке etl. Конфигурационная папка проекта config, папка приложения etl_app. Про остальные файлы и папки читайте далее.

.           
├── Makefile
├── Vagrantfile
├── etl
│   ├── README.md
│   ├── __init__.py
│   ├── config
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── celery.py
│   │   ├── settings.py
│   │   ├── settings_local.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── etl_app
│   │   ├── __init__.py
│   │   ├── apps.py
│   │   ├── etl.py
│   │   └── tasks.py
│   └── manage.py
├── poetry.lock
├── pyproject.toml
└── vagrant
    ├── playbook.yml
    ├── postgres.yml
    ├── requirements.yml
    └── templates
        └── settings_local.j2

Начинаем с Ansible

Ansible — система управления конфигурациями для Linux. Конфигурировать мы будем создаваемую виртуальную машину через сценарий (далее плейбуки) в формате YAML с описанием требуемых состояний управляемой системы. Сценарий — это описание состояния ресурсов системы, в котором она должна находиться в конкретный момент времени, включая установленные пакеты, запущенные службы, созданные файлы и многое другое. Ansible проверяет, что каждый из ресурсов системы находится в ожидаемом состоянии и пытается исправить состояние ресурса, если оно не соответствует ожидаемому.

Для выполнения задач используется система модулей. Каждая задача представляет собой её имя, используемый модуль и список параметров, характеризующих задачу. Система поддерживает переменные, фильтры обработки переменных (поддержка осуществляется библиотекой Jinja2), условное выполнение задач, параллелизацию, шаблоны файлов. Адреса и настройки целевых систем содержатся в файлах «инвентаря» (inventory). Поддерживается группирование. Для реализации набора однотипных задач существует система ролей.

Из всего вышеперечисленного мы воспользуемся ролями, чтобы писать меньше кода, и шаблонизатором Jinja2.

Все плейбуки пишем в папке vagrant.

requirements.txt

В нашем проекте будет использована роль python_developer из репозитория ролей ansible-galaxy. Она (роль) представляет из себя готовый набор задач, которые установят для нас pyenv и poetry. Прописываем роль в файле зависимостей requirements.txt:

- name: staticdev.python-developer
  src: https://github.com/staticdev/ansible-role-python-developer.git

postgres.yml

В этом плейбуке мы пропишем набор задачи по созданию базы данных и схем в ней:

---
- name: Add postgreuser permission for createbd
  become: true
  become_user: postgres
  shell: psql -U postgres -c 'ALTER USER {{ postgres_user }} CREATEDB'

- name: Create database
  become: true
  become_user: postgres
  postgresql_db:
    name: "{{ postgres_db }}"
    encoding: UTF-8
    owner: "{{ postgres_user }}"

- name: Create schemas
  become: true
  become_user: postgres
  community.postgresql.postgresql_schema:
    db: "{{ postgres_db }}"
    name:
      - main
      - my_schema
    owner: "{{ postgres_user }}"

Обратите внимание на строки вида {{ some_var }} - это переменные. Их мы зададим в следующем плейбуке

playbook.yml

Здесь мы пишем наш основной сценарий. Он определяет - что надо установить на нашу виртуальную машину, а также использует описанные ранее плейбуки.

---
- hosts: all
  vars:
    app_settings_dir: config
    postgres_user: sergei
    postgres_password: sergei
    postgres_db: demo
    python_version: 3.10.6
  roles:
    - role: staticdev.python-developer
      vars:
        pyenv_global:                   
          - "{{ python_version }}"
        pyenv_python_versions:
          - "{{ python_version }}"
  tasks:
    - name: add GPG key
      ansible.builtin.apt_key:
        url: https://keyserver.ubuntu.com/pks/lookup?op=get&fingerprint=on&search=0x1655A0AB68576280
        id: 68576280
        state: present
      become: yes

    - name: Install base needed apt-packages
      ansible.builtin.apt:
        pkg:
          - wget
          - unzip
          - redis-server
          - wget
          - python3-psycopg2
          - acl
        state: latest
      become: yes
      become_method: sudo

    - name: Set up Postgres repo
      shell: |
        echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
        wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
      args:
        warn: no
      become: yes

    - name: Install postgresql
      apt:
        name: postgresql-15
        update_cache: yes
      become: yes

    - name: Start and enable postgres
      ansible.builtin.service:
        name: postgresql
        enabled: yes
        state: started
      become: yes

    - name: Create user
      community.postgresql.postgresql_user:
        name: "{{ postgres_user }}"
        password: "{{ postgres_password }}"
      become: yes
      become_user: postgres

    - name: Create db
      include_tasks: postgres.yml

    - name: Ensure Redis is started
      service: name=redis-server state=started enabled=yes
      become: yes
      become_method: sudo

    - name: Exporting broker url to env
      lineinfile:
        dest: "/etc/environment"
        state: present
        regexp: "^REDIS_URL="
        line: "REDIS_URL=redis://127.0.0.1:6379/0"
      become: yes
      become_method: sudo

    - name: Exporting db url to env
      lineinfile:
        dest: "/etc/environment"
        state: present
        regexp: "^DATABASE_URL="
        line: "DATABASE_URL=postgres://{{ postgres_user }}:{{ postgres_password }}@127.0.0.1:5432/{{ postgres_db }}"
      become: yes
      become_method: sudo

    - name: Make settings file from template
      template:
        src: /vagrant/vagrant/templates/settings_local.j2
        dest: "/vagrant/etl/{{ app_settings_dir }}/settings_local.py"

    - name: set python version for project
      ansible.builtin.command: /home/vagrant/.local/bin/poetry env use /home/vagrant/pyenv/shims/python
      args:
        chdir: /vagrant/

Разделы плейбука:

  • vars - задаем переменные

  • roles - запускаем роли (предопределенный набор задач). pyenv_python_versions - список версий питона, которые вы хотите установить на ВМ, pyenv_global- какую из этих версий сделать глобальной.

  • tasks - перечень задач, которые образуют набор консольных команд. Только таски ещё отслеживают ответы выполняемых команд. Если какая-либа из команд вернут ошибку, то выполнение плейбука прекратится. В примере выше я устанавливаю Redis (строки 29 и 73). Вы вольны устанавливать любой набор пакетов.

Чуть подробнее остановимся на задаче Make settings file from template. Этой команда берет шаблон файла settings_local.j2 по пути src, прогоняет его через шаблонизатор Jinja2 - см. DEBUG в фрагменте кода ниже. Затем копирует готовый модуль в расположение dest. Самая очевидная польза, это возможность исключить файл settings_local.py конфигурационной папки проекта из индекса гита. Как следствие - settings_local.py это действительно локальный (простите за тавтологию) модуль. Такая методика позволяет для прода задействовать другие значения переменных.

Был settings_local.j2:

import os
import dj_database_url

DEBUG = {{ debug|lower|capitalize }}

...

Стал settings_local.py:

"""Локальные настройки проекта"""
import os
import dj_database_url

DEBUG = True

...

Готовим к запуску виртуальную машину Vagrant

Для запуска виртуальной машины нам потребуется Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"

  config.vm.network "forwarded_port", guest: 8000, host: 8001
  config.vm.network "forwarded_port", guest: 5432, host: 5433
  config.vm.provider "virtualbox" do |v|
    v.customize ["modifyvm", :id, "--uart1", "0x3F8", "4"]
    v.customize ["modifyvm", :id, "--uartmode1", "file", File::NULL]
    v.memory = 2048
  end

  # ansible_local
  config.vm.provision "ansible_local" do |ansible|
    ansible.verbose = "vv"
    ansible.galaxy_role_file = 'vagrant/requirements.yml'
    ansible.galaxy_roles_path = '/home/vagrant/.ansible/roles'
    ansible.galaxy_command = 'ansible-galaxy install --ignore-errors --force --role-file=%{role_file}'
    ansible.playbook = "vagrant/playbook.yml"
  end
end

Пояснения по номерам строк:

  • 5 - образ операционной системы

  • 7- проброс 8000-ый порт ВМ, который как правило используется для запуска сервера разработки Django, на 8001-ый порт хостовой машины. Оба порта могут быть изменены по вашему усмотрению.

  • 8 - порт 5432 СУБД Postgres пробрасываем на порт 5433 хостовой машины. Порт 5433 может быть изменен по вашему усмотрению.

  • 12 - объем оперативной памяти ВМ в Мбайтах.

  • 16 - объявляем блок команд для запуска ansible.

  • 17 - уровень детализации в консоли, регулируется количеством символов v.

  • 18 - путь файлам зависимостей.

  • 19 - путь для хранения файлов ролей asnible. Подробнее об этом в документации. В данному случае они будут хранится в папке пользователя и не фигурировать в проекте.

  • 20 - консольная команда установки зависимостей ansible. Я её "кастомизировал" через--ignore_errors, чтобы не было ошибки при повторной установке зависимостей.

  • 21 - путь до основного плейбука, который и запускает всю "магию".

Запускаем виртуальную машину

Создание и каждый последующий запуск производится с помощью консольной команды vagrant up. Полный алгоритм выглядит так:

  1. В директории проекта запустить консольную команду vagrant up.

  2. Сразу (не дожидаясь выполнения п.1) запустить окно Oracle VM virtual box. Сделать активной создаваемую виртуальную машину (дополнительных окон открывать не надо). До полного запуска ВМ это окно держать активным.

Первый запуск может длиться 15-20 минут, потому что идет создание ВМ и установка пакетов. В это время следите, чтобы компьютер не переходил в "сон". Возможно для первого запуска понадобится VPN.

Другие команды для управления ВМ:

  • vagrant halt - остановить ВМ

  • vagrant ssh - зайти на ВМ

  • vagrant destroy - "разобрать" ВМ

Используем make

Для работы с виртуальной машиной нам достаточно команд из предыдущего раздела. Но дополнительно мы можем наиболее часто употребимые вынести в make. Процесс установки на Ubuntu описан здесь, на Windows здесь.

Создадим Makefile:

# инициализировать виртуальную машину
up:
	vagrant up

# установить зависимости проекта
install:
	vagrant ssh -c "cd /vagrant/ && poetry install"

# сделать дата-миграции бд
migrate:
	vagrant ssh -c "cd /vagrant/etl && poetry run python manage.py makemigrations && poetry run python manage.py migrate"

# запустить сервер разработки
runserver:
	vagrant ssh -c "cd /vagrant/etl && poetry run python manage.py runserver 0.0.0.0:8000"

Обратите внимание на последнюю команду. Благодаря пробросу портов в Vagrantfile сервер разработки будет доступен на хостовой машине по адресу http://localhost:8001.

Настраиваем IDE

Для полноценной интеграции интерпретатора в оболочку IDE, последние должны уметь работать через ssh-подключение. Параметры для подключения:

  • host 127.0.0.1

  • port 2222. Если вы одновременно запускаете не одну ВМ, то у каждой последующей будет другой порт. Будьте внимательны. Порт вы увидите в отчете команды по инициализации ВМ.

  • username vagrant

  • key pair (подключение идет по ключу, путь относительно директории, где находится Vagrantfile): .vagrant/machines/default/virtualbox/private_key

PyCharm Pro

К сожалению, интеграция с интерпретатором в версии Community исключена, т.к. отсутствует функционал ssh-подключения. В этом случае можно в терминале зайти на виртуальную машину и все запуски кода производить из консоли виртуальной машины.

Подключение к удаленной БД в Community также невозможно. Но pgAdmin (более чем) полноценно заменит функционал PyCharm Pro в этой части.

Настройка интерпретатора

Инструкция доступна по ссылке на сайте разработчика IDE. Дополнительное к параметрам подключения по ssh, нам понадобится путь до интерпретатора внутри ВМ. Его можно получить составной командой в консоли виртуальной машиныcd /vagrant/ && poetry run which python

Подключение к БД

К базе данных подключаемся также по ssh. Инструкция доступна по ссылке на сайте разработчика IDE. Данные для подключения возьмите из начала текущего раздела. Логин/пароль/имя БД у нас указаны в playbook.yml в разделе vars.

VS Code

Интеграция с интерпретатором производится в два шага:

1) Зафиксируйте настройку подключения к в ВМ в конфигурации ssh-клиента вашей хостовой машины. Для этот выполните команду vagrant ssh-config на хостовой машине в папке проекта. Вывод будет примерно такой:

Host default
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile <some path>/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

Добавьте эти строки в конфигурационный файл вашего ssh-клиента. Как правило это ~/.ssh/config. default замените на что-либо вам понятное и относящееся к проекту.

2) Настройте в VS Code интерпретатор по ssh-подключению. Предварительно убедитесь, что у вас установлены плагины Remote SSH и Python от Microsoft.

Подключаемся к ВМ:

В консоли виртуальной машины составной командойcd /vagrant/ && poetry run which python получаем путь до интерпретатора. Например /home/vagrant/.cache/pypoetry/virtualenvs/etl-base-vs8V2ZPt-py3.10/bin/python

Указываем этот путь в настройках плагина Python:

в окне поиска набираем > и выбираем Python: Select Interpreter
в окне поиска набираем > и выбираем Python: Select Interpreter
Вставляем полученный ранее путь до интерпретатора
Вставляем полученный ранее путь до интерпретатора

Подробная инструкция есть по ссылке на сайте разработчика IDE.

Подводим итоги

Мы создали виртуальную машину без нагрузки в виде GUI. Показали как можно работать с ней с помощью программ, установленных на хостовой машине. Для "полного счастья" можно установить ssh-клиент с графическим интерфейсом, чтобы управлять файлами в привычной среде, как вариант - MobaXterm.

В начале статьи я обещал, что каждый участник сможет развернуть ваш проект у себя на машине за четыре консольные команды. Вот они:

git clone <repo>  # клонируем репозиторий
make up  # поднимаем ВМ
make install  # устанавливаем зависимости проекта
make migrate  # создаем требуемые таблицы базы данных

Благодарю за внимание, жду ваших комментариев и с удовольствием отвечу на все вопросы.

Комментарии (7)


  1. Hrodvitnir
    00.00.0000 00:00
    +3

    Мне кажется, что лечить отсутствие квалификации Vagrant'om это некоторый оверхед. Тем более, что контейнер тоже настроил, а потом просто подвязываешь к нему IDE.

    Но с Vagrant мы так работали тоже, но тогда не было Docker:) Ну или, если точнее, он не был так популярен


  1. amkartashov
    00.00.0000 00:00

    Я рекомендую связку tilt + docker for desktop + vscode для локального окружения.

    * врубаем kubernetes engine в докере
    * используем helm_resource для деплоя зависимостей (куча готовых чартов для postgres/redis/ и тд)
    * docker_build для образа своего софта
    * свой софт деплоится через тот же helm_resource или через k8s_yaml
    * дополнительно можно ingress_nginx задеплоить и выставлять наружу (на локалхост) свои сервисы через .7f000001.nip.io. Если хочется потестить с честным SSL - get.localhost.localdirect

    Сложная часть обычно c удалённым дебагом. Проще всего с go, с python придётся повозиться, для poetry проектов надо debugpy в зависимости подключать и запускать через poetry run python -m debugpy --listen 0.0.0.0:5679 src/main.py arg1 arg2 arg3

    Плюсы по сравнению с вагрантом:

    * можно в куб и докер
    * можно организовать hot reload с локального кода, а не разрабатывать внутри вм
    * можно локально сервисы заавтоматизировать таким же образом
    * меньше оверхед иногда. Иногда наоборот :)
    * не надо ансибл


  1. molybdenum
    00.00.0000 00:00
    +2

    Виртуальная машина Vagrant

    <занудство-on>

    Vagrant не является вирутальной машиной. Врутальные машины создает Oracle VritualBox, а Vagrant это просто обёртка-менеджер, которая позволяет легко их создавать, конфигурировать и уничтожать.

    <занудство-off>


    1. Urvin
      00.00.0000 00:00
      +1

      <душнила-mode>
      Вообще-то в в Vagrant можно подключать различные бекенды виртуальных машин, не обязательно VritualBox
      </душнила-mode>


      1. SergeyKlimov_Creator Автор
        00.00.0000 00:00

        <точно так>

        в том числе docker

        </точно так>


  1. AYamangulov
    00.00.0000 00:00

    В качестве альтернативы виртуальной машине можно использовать docker-контейнеры. Но есть нюанс, что для работы с контейнерами требуется соответствующая квалификация.

    Прошу обратить внимание, что для выполнения всех действий в статье также требуется альтернативная квалификация (только в Vagrant + Ansible, как минимум). А особенно - что перечень описанных действий, на мой взгляд, существенно длиннее, чем если поднять локальный стенд на docker + что-нибудь вспомогательное, например, docker-compose, плюс расшарить общий проект с ветками где-нибудь в github/gilab/bitbucket. И это совсем не сложно, стек всем известен и хорошо отлажен. А вот о vagrant не могу сказать, чтобы он был столь же распространенным и активно использовался бы. ИМХО - как раз для него небольшое самообучение потребуется.

    Безусловно, автор проделал хорошую работу и подробно описал интересный кейс. Возможно, также, что в каких-то случаях, действительно, возможны объективные причины, которые КАТЕГОРИЧЕСКИ требуют использования vagrant vs docker, где он имеет какие-то явные и реальные преимущества. Только я, например, таких преимуществ особо не нашел, может быть, кто-то их действительно знает? - Тогда прошу их описать, хотя бы в комментариях.


    1. SergeyKlimov_Creator Автор
      00.00.0000 00:00

      Здесь важен контекст. Речь идёт о группах, в которых были выпускники курсов. Я использовал два варианта - на докере и на вагранте. Вагрант требовал от меня больше знаний и объяснений на входе в проект. Потом вопросов "как это мне запустить?" было минимум, потому что участники работали с привычным интерфейсом (по другому я это объяснить не могу). С докером было наоборот. Сначала приходилось объяснять что-такое докер, а затем попутно решать вопросы "а у меня слетело, а я перезапустил" и т.д.