Vagrant virtual machine for team development

The topic of creating vagrant virtual machines has already been discussed on Habré with comments “why do I need this?”. In my article I want to answer this question. When you have a project and new employees join your team along the way, the problem arises that they do not have identical conditions to run and test the project on their machines. And here the Vagrant virtual machine comes to the rescue. Using the example of the django project created in my etl article, I want to show how each new member of your project will deploy a development environment with all dependencies in four console commands. This will allow you to start the work process as soon as possible.

As an alternative to a virtual machine, you can use docker containers. But there is a caveat that to work with containers, appropriate qualifications are required.

So, let’s imagine that we have a django project that we want to develop as a team.

We will create a virtual machine for further development with preinstalled pyenv for python versioning and poetry to manage dependencies. We will also configure the interpreter in the IDE PyCharmPro And VS Code.

All sources from this article are posted in repository.

Installing the software

On the host (main) operating system, each project participant must have:

Let’s designate the structure of the project

The little django project is in the etl folder. Project configuration folder config, application folder etl_app. Read about other files and folders below.

.           
├── 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

Getting started with Ansible

Ansible is a configuration management system for Linux. We will configure the created virtual machine through a script (hereinafter referred to as playbooks) in the format YAML with a description of the required states of the controlled system. A script is a description of the state of a system’s resources that it should be in at a given time, including packages installed, services running, files created, and more. Ansible checks that each of the resources in the system is in the expected state and tries to fix the state of the resource if it is not as expected.

To perform tasks, a system of modules is used. Each task is its name, the module used and a list of parameters that characterize the task. The system supports variables, filters for processing variables (support is provided by the library Jinja2), conditional task execution, parallelization, file patterns. The addresses and settings of the target systems are contained in the “inventory” files. Grouping is supported. To implement a set of tasks of the same type, there is a system of roles.

From all of the above, we will use roles to write less code, and a templating engine Jinja2.

We write all playbooks in the vagrant folder.

requirements.txt

In our project, the role will be used python_developer from the roles repository ansible-galaxy. She (role) is a ready-made set of tasks that will install pyenv and poetry for us. We register the role in the requirements.txt dependency file:

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

postgres.yml

In this playbook, we will write a set of tasks for creating a database and schemas in it:

---
- 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 }}"

Pay attention to lines like {{ some_var }} – these are variables. We will ask them in the next playbook.

playbook.yml

This is where we write our main script. It determines what needs to be installed on our virtual machine, and also uses the playbooks described earlier.

---
- 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/

Playbook sections:

Let’s take a closer look at the problem Make settings file from template. This command takes the settings_local.j2 file template from the src path, runs it through the Jinja2 template engine – see DEBUG in the code snippet below. It then copies the completed module to the dest location. The most obvious benefit is the ability to exclude the settings_local.py file of the project’s configuration folder from the git index. As a consequence, settings_local.py is indeed a local (sorry for the tautology) module. This technique allows you to use other variable values ​​for the sale.

Was settings_local.j2:

import os
import dj_database_url

DEBUG = {{ debug|lower|capitalize }}

...

Became settings_local.py:

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

DEBUG = True

...

Preparing to launch the Vagrant virtual machine

To start the virtual machine, we need a 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

Line number explanations:

Starting the virtual machine

Creation and each subsequent launch is done using the console command vagrant up. The complete algorithm looks like this:

  1. In the project directory, run the console command vagrant up.

  2. Immediately (without waiting for step 1) launch the Oracle VM virtual box window. Make the created virtual machine active (no additional windows need to be opened). Until the VM is fully launched, keep this window active.

The first launch can take 15-20 minutes because the VM is being created and packages are being installed. At this time, make sure that the computer does not go into “sleep”. You may need a VPN for the first run.

Other commands for managing the VM:

Using make

To work with a virtual machine, the commands from the previous section are enough for us. But in addition, we can take out the most commonly used ones in make. The installation process on Ubuntu is described Hereon Windows Here.

Let’s create a 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"

Pay attention to the last command. Thanks to port forwarding in Vagrantfile, the development server will be available on the host machine at http://localhost:8001.

Setting up the IDE

To fully integrate the interpreter into the IDE shell, the latter must be able to work through an ssh connection. Connection options:

  • host 127.0.0.1

  • port 2222. If you start more than one VM at the same time, then each subsequent one will have a different port. Be careful. You will see the port in the report of the VM initialization command.

  • username vagrant

  • key pair (connection goes by key, the path is relative to the directory where the Vagrantfile is located): .vagrant/machines/default/virtualbox/private_key

PyCharmPro

Unfortunately, integration with the interpreter in the Community version is excluded, because there is no ssh connection functionality. In this case, you can go to the virtual machine in the terminal and perform all code launches from the virtual machine console.

Connecting to a remote database in Community is also not possible. But pgAdmin (more than) fully replace the functionality of PyCharm Pro in this part.

Setting up an interpreter

The instruction is available at link on the IDE developer site. In addition to the ssh connection parameters, we need the path to the interpreter inside the VM. It can be obtained with a compound command in the virtual machine consolecd /vagrant/ && poetry run which python

Database connection

We also connect to the database via ssh. The instruction is available at link on the IDE developer site. Get connection information from the beginning of the current section. Login / password / database name we have specified in playbook.yml in the vars section.

VS Code

Integration with the interpreter is performed in two steps:

1) Fix the connection setting to in the VM in the ssh client configuration of your host machine. For this run the command vagrant ssh-config on the host machine in the project folder. The output will be something like this:

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

Add these lines to your ssh client config file. This is usually ~/.ssh/config. default replace with something you understand and related to the project.

2) Set up an ssh connection interpreter in VS Code. Make sure you have Microsoft’s Remote SSH and Python plugins installed first.

Connecting to VM:

In the virtual machine console with a compound commandcd /vagrant/ && poetry run which python get the path to the interpreter. For example /home/vagrant/.cache/pypoetry/virtualenvs/etl-base-vs8V2ZPt-py3.10/bin/python

Specify this path in the Python plugin settings:

in the search box, type > and select Python: Select Interpreter” title=”in the search box, type > and select Python: Select Interpreter” width=”1120″ height=”108″ data-src=”https://habrastorage.org/getpro/habr/upload_files/f30/2b4/3e9/f302b43e974117b629e5b89b55b5298d.png”/><figcaption>in the search box, type > and select Python: Select Interpreter</figcaption></figure><figure class=Insert the previously obtained path to the interpreter
Insert the previously obtained path to the interpreter

Detailed instructions are available at link on the IDE developer site.

Summing up

We have created a virtual machine with no load in the form of a GUI. They showed how you can work with it using programs installed on the host machine. For “full happiness” you can install a ssh client with a graphical interface to manage files in a familiar environment, as an option – MobaXterm.

At the beginning of the article, I promised that each participant will be able to deploy your project on their machine in four console commands. Here they are:

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

Thank you for your attention, I look forward to your comments and will be happy to answer all questions.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *