Запуск/отладка Python скриптов в контейнерах LXC/LXD из под VS Code +9


Привет, Хабр!

В этой статье хочется поделится с вами настройкой девелоперского окружения под разработку скриптов на Python и запуск их в изолированном окружении, практически за пару кликов в редакторе VS Code.

В качестве изолированного окружения будет рассмотрена первичная настройка и создание контейнера на базе LXC под управлением ОС Ubuntu 17.10. Почему не Docker? Сложно ответить, выдать какие-то аргументы. От части концепция виртуализации ОС субъективно ближе по духу, чем виртуализация процессов, но принцип который здесь будет изложен, с некоторыми поправками, подойдет к запуску кода в Docker-контейнерах.



Что вы получите?


  • Научитесь запускать в один клик python-скрипты в контейнере, а также удаленный debug из VS Code
  • Узнаете как установить LXD, настроить сеть и запустить первый контейнер
  • Ознакомитесь с установкой и настройкой редактора исходного кода VS Code, набором расширений которые позволят комфортно писать и отлаживать код.

Чего здесь не будет?


  • Не будет полного обзора LXC/LXD, вам придется самостоятельно найти дополнительную информацию о Linux-контейнерах.
  • Не будет рассмотрен python модуль virtualenv для создания виртуального окружения на хосте разработчика как альтернатива контейнерам.
  • Не будет сравнения в духе Docker vs LXD, выводы вы сделаете самостоятельно на основании вашего опыта.

Содержание:



Выбор backend хранилища


Перед установкой LXD необходимо определиться, какой тип backend хранилища для контейнеров будет использоваться. Позже изменить будет нельзя. Доступно несколько вариантов: ZFS, btrfs, LVM или файловая система хоста. Для сравнения возможностей доступна таблица сравнения в официальной документации. Предлагаю выбрать ZFS который позволит в будущем легко создавать COW (copy-on-write) снимки (snapshots) контейнеров. Установим в системе пакет утилит работы с ZFS:

$ sudo apt install zfsutils-linux

Установка пакета LXD


Здесь нет сложностей, пакет входит в стандартную поставку системы, поэтому командуем в консоли:


$ sudo apt install lxd

После установки LXD, текущий пользователь будет добавлен в системную группу lxd, но чтобы продолжить настройку с необходимыми привелегиями в системе, необходимо сейчас выполнить переактивацию сессии пользователя. Просто выйдите и заново войдите в сессию вашего Desktop Meneger’а или перезагрузитесь полностью. Проверить, состоит ли текущая сессия пользователя в группе lxd можно через команду id:


$ id 
uid=1000(user1) gid=1000(user1) groups=1000(user1),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),118(lpadmin),128(sambashare),129(lxd)

Если вы видите группу lxd, то всё впорядке, можно продолжать дальше.

Инициализация LXD


Инициализация LXD проходит в интерактивном режиме от имени текущего пользователя который состоит в группе lxd. Выполните команду и следуйте инструкциям и комментариям представленным ниже:


$ lxd init
 
# Здесь нас спрашивают, хотим ли мы сейчас создать пул хранилища? Не путать с пулом ZFS!
# По умолчанию ответ <yes>, нажимаем <ENTER>
Do you want to configure a new storage pool (yes/no) [default=yes]?
 
# Задаем желаемое имя для пула хранилища и нажимаем <ENTER>
Name of the new storage pool [default=default]: lxdpool
 
# Здесь нам нужно определить тип пула хранилища, выбираем ZFS
Name of the storage backend to use (dir, lvm, zfs) [default=zfs]: zfs
 
# Создать сейчас ZFS пул? Т.е. в пуле хранилища lxdpool создастся ZFS пул.
# Соглашаемся и нажимаем <ENTER>
Create a new ZFS pool (yes/no) [default=yes]?
 
# Здесь спрашивают, хотим ли мы использовать существующее блочное устройство?
# Например, это может быть ранее нами подготовленный файл созданный через утилиту dd
# или это может быть отдельный жеский диск или партиция.
# По умолчанию ответ равен <no>, жмем <ENTER>
Would you like to use an existing block device (yes/no) [default=no]?
 
# Каким размером создать loop-устройство (zfs pool)?
# Указываем число в GB, минимально 1GB
Size in GB of the new loop device (1GB minimum) [default=15GB]: 5
 
# Желаете сделать LXD доступным по сети?
# По умолчанию LXD не слушает сеть, единственный способ говорить с ним - через локальный unix сокет /var/lib/lxd/unix.socket
# Отвечаем <no>, жмем <ENTER>:
Would you like LXD to be available over the network (yes/no) [default=no]?
 
# Желаете чтобы образы в локальном репозитории автоматически обновлялись?
Would you like stale cached images to be updated automatically (yes/no) [default=yes]? no
 
# Желаете сейчас создать сетевой мост? Отвечаем <no>, мы его позже создадим и настроим
Would you like to create a new network bridge (yes/no) [default=yes]? no
 
# LXD has been successfully configured.

Конфигурация сети LXС


Если сеть не настроить сейчас, а приступить к созданию контейнеров, то при их создании мы получим предупреждение, что контейнер не присоединен ни к одной из сетей. Это не будет являться ошибкой, просто к ним не будет сетевого доступа через интерфейсы. В начале можно создать необходимое количество контейнеров, а затем сконфигурировать сеть и присоединить к ней контейнеры с помощью команды lxc network attach. Сейчас нам необходимо определится, какой тип сети строить. Доступно несколько вариантов построения LXC-сетей, мы же выберем вариант bridge (простой программный коммутатор между хостом и контейнерами) и построим сеть на его основе.

При создании сетевого моста мы можем сразу задать свойства после указания его имени или положиться на автоконфигурацию которая задаст подсеть IPv4 и IPv6 случайным выбором. По умолчанию, параметр равен ipv4.nat=false и по каким-то причинам пакеты транзитного трафика с контейнеров не будут маршрутизироваться дальше хоста на другие локальные сегменты сети даже тогда, когда в ядре хоста явно включить IP forwarding. Видимо NAT в LXD больше чем транслятор адресов с локальных сегментов сети в глобальные, так как при активации этого параметра также активируется IP forwarding на хосте, поэтому мы явно его включим:


$ lxc network create lxdbr0 ipv6.address=none ipv4.address=10.0.3.1/24 ipv4.nat=true
Network lxdbr0 created
$ lxc network list
+--------+----------+---------+-------------+---------+
|  NAME  |   TYPE   | MANAGED | DESCRIPTION | USED BY |
+--------+----------+---------+-------------+---------+
| ens33  | physical | NO      |             | 0       |
+--------+----------+---------+-------------+---------+
| lxdbr0 | bridge   | YES     |             | 0       |
+--------+----------+---------+-------------+---------+

Создание профиля конфигурации контейнера


Конфигурация контейнера условно делится на два уровня, глобальная и локальная. Глобальная конфигурация задается в профиле конфигурации который присоединяется к конкретному контейнеру или группе контейнеров, а локальная конфигурация применяется только для конкретного контейнера. Т.е., все установки в профиле наследуются контейнером, а локальные установки имеют приоритет над профилем. К одному контейнеру можно присоединить несколько профилей.

Создадим профиль LXD для того, чтобы во вновь создаваемых контейнерах по умолчанию был интерфейс типа nic который будет слинкован с коммутатором (сетевым мостом) lxdbr0 на хосте:


$ lxc profile copy default network0
$ lxc profile device add network0 eth0 nic parent=lxdbr0 nictype=bridged

Создание контейнера


Для примера создадим контейнер и назовем его pytest, который будет доступен из хоста по этому имени. Выполним команду launch которая загрузит образ из репозитория ubuntu: версию 17.10 и архитектуры amd64 и из этого образа создаст контейнер. Дополнительно, опцией укажем, что этому контейнеру необходимо задать конфигурацию из профиля network0:


$ lxc launch ubuntu:17.10/amd64 --profile network0 pytest

Далее пойдет загрузка из сети образа. Немного подождем, а чуть позже можем увидеть доступные контейнеры в системе и их состояние:


$ lxc list
+--------+---------+------------------+------+------------+-----------+
|  NAME  |  STATE  |       IPV4       | IPV6 |    TYPE    | SNAPSHOTS |
+--------+---------+------------------+------+------------+-----------+
| pytest | RUNNING | 10.0.3.13 (eth0) |      | PERSISTENT | 0         |
+--------+---------+------------------+------+------------+-----------+

Установка ПО в контейнере


Здесь мы с вами установим необходимое ПО в контейнере. Запустим внутри контейнера пакетный менеджер который актуализирует систему:


$ lxc exec pytest -- /bin/bash -c 'apt update && apt upgrade -y'

Следом, установим менеджер пакетов pip (Python package installer), а также python-модуль ptvsd (Visual Studio remote debugging server for Python). Модуль ptvsd необходимо установить обязательно версии 3.0.0, на более свежих версиях (актуальная 3.2.1.0) я получил ошибку, которая обозначена в issue на github.com. Автор расширения Python к VS Code которое взаимодействует с ptvsd как временное решение рекомендует использовать версию 3.0.0.


$ lxc exec pytest -- /bin/bash -c 'apt install python3-pip -y'
$ lxc exec pytest -- /bin/bash -c 'pip3 install ptvsd==3.0.0'

По части LXD мы с вами завершили настройку.

Установка VS Code и расширений


Варианты установки приложения описаны на официальной страничке по адресу code.visualstudio.com/docs/setup/linux, мы же немного модифицируем команды:


$ cd ~
$ wget -c https://packages.microsoft.com/keys/microsoft.asc
$ cat microsoft.asc | gpg --dearmor > microsoft.gpg
$ sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
$ sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list'
$ sudo apt update
$ sudo apt install code

После выполнения у нас в системе появится VS Code последней версии из репозитория Microsoft.

P.S. На момент написания статьи вышла новая версия 1.18.0 в которой, в стабильную сборку вошли на мой взгляд одни из наиболее ожидаемых (2 года) сообществом возможности «Multi-root workspaces» и «Git indicators in the Explorer» Обо всех изменениях можно почитать на этой страничке.

Приступим к установке расширений. Если вы впервые устанавливаете VS Code, например, после использования Atom (как автор), то рекомендую обратить внимание на расширения в списке ниже, а для достижения цели этой статьи будет достаточно установить только Python и Code Runner. Расширения устанавливаются с marketplace'а в левой части редактора.


Настройка конфигурации VS Code


Следующие настройки необязательны, они прямо не относятся к запуску Python скриптов в контейнерах, но это приблизит вас к тому, если хотите чтобы было визуально как в Atom'е.

В открытом редакторе откройте настроки редактора: file->Preferences->Settings. В открывшемся окне, справа, в USER_SETTINGS внесите следующие изменения и сохраните (Ctrl + S):

{
    // Общие настройки
    "telemetry.enableTelemetry": false,
    "editor.fontFamily": "Menlo, Consolas, 'DejaVu Sans Mono', monospace",
    "editor.lineHeight": 21,
    "editor.minimap.enabled": true,
    "editor.minimap.showSlider": "always",
    "editor.minimap.renderCharacters": false,
    "files.insertFinalNewline": true,
    "files.trimFinalNewlines": true,
    "explorer.openEditors.visible": 0,

    // Настройка интегрированного терминала:
    "terminal.integrated.lineHeight": 1.25,
    "terminal.integrated.scrollback": 2000,

    // Настройка темы:
    "workbench.iconTheme": "file-icons",
    "workbench.colorTheme": "One Dark Pro",
    "workbench.colorCustomizations": {
        // https://code.visualstudio.com/docs/getstarted/theme-color-reference
        "editor.lineHighlightBackground": "#2C323C"
    },

    // Настройка подсветки скобок
    "editor.matchBrackets": false,
    "subtleBrackets.styles": {
        "global": {
            "color": "#61afef",
            "borderColor": "#61afef",
            "borderWidth": "1px",
            "borderStyle": "none none solid none"
        }
    },

    // Python
    "python.pythonPath": "python3",
    
    // Настройка кодо-запускателя:
    "code-runner.executorMap": {
        "python": "python3 $fileName"
    }
}

Линковка каталогов Хост <-> Контейнер


Мы с вами уже проделали большую работу и настало время заключительной части. Сейчас у нас почти всё готово для запуска скриптов в контейнере.

Создадим каталог projects, например, по адресу /home/user1/projects. В нём будут располагаться наши будущие python-проекты. Этот каталог мы слинкуем с контейнером, т.е., в контейнере создадим устройство типа disk и свяжим этот каталог с внутренним /mnt/hostfs. Выполняем следующие команды:


$ mkdir ~/projects
$ lxc config device add pytest hostfs disk path=/mnt/hostfs source=/home/user1/projects/

Если мы локально создадим каталог или файл в каталоге /home/user1/projects, то он немедленно отразится в контейнере по адресу /mnt/hostfs. Убедиться в этом можно запустив интерактивно оболочку BASH в контейнере и выполнив команду ls:


$ lxc exec pytest -- /bin/bash
$ ls -l /mnt/hostfs/

Настройка VS Code на запуск скриптов в контейнере


И так, сейчас мы можем вернуться в наш редактор, если вы его закрыли, то вновь откройте. Нажмите в меню File -> New File. Создастся новая вкладка в которую нужно внести следующий python-код, который мы будем запускать в контейнере. Сохраните его в каталоге /home/user1/projects/pytest/test.py, а затем файл (вкладку) можно закрыть.

import os

#### BEGIN. Visual Studio remote debugging server for Python
# import ptvsd

# print('Waiting connection on port 3000')
# ptvsd.enable_attach('my_secret', address=('0.0.0.0', 3000))
# ptvsd.wait_for_attach()
# ptvsd.break_into_debugger()
#### END. Visual Studio remote debugging server for Python


with open('/etc/lsb-release') as f:
     lsb_release = f.readlines()

info = {
    'HOSTNAME': os.uname().nodename,
    'DISTRIB': lsb_release[3][21:-2],
    'USER': os.environ['USER'],
    'LOGNAME': os.environ['LOGNAME'],
    'HOME': os.environ['HOME'],
    'PWD': os.environ['PWD']
}

print(info)


VS Code «скажет», что у вас не установлен модуль pylint, но он нам сейчас не нужен, вы его можете установить позже, самостоятельно, через менеджер модулей pip. Сейчас нужно открыть workspace в VS Code, для этого слева в File Explorer нажмите кнопку Open Folder и выберите каталог /home/user1/projects/pytest/:



VS Code переинициализируется, полностью закроются все окна и откроется workspace в котором будет виден файл test.py. Откройте его, дважды кликнув на имя слева в File Explorer VS Code.
Вызовите настройки workspace'а, нажав в меню File -> Preferences -> Setting, далее в окне справа вверху вместо USER_SETTINGS выберите из выпадающего списка WORKSPACE_SETTINGS:



Внесите и сохраните следующие настройки так, чтобы у вас получилось вот так:


{
    "code-runner.executorMap": {
        // Запуск от имени пользователя root:
        // "python": "lxc exec pytest -- /bin/bash -c 'python3 /mnt/hostfs/pytest/$fileName'"
        // Запуск от имени встроенного в контейнер пользователя ubuntu:
        "python": "lxc exec pytest -- /bin/bash -c 'su -l ubuntu -c \"python3 /mnt/hostfs/pytest/$fileName\"'"
    },
    "code-runner.runInTerminal": true
}

В файловом эксплорере вы можете обнаружить файл settings.json в каталоге .vscode, там сохраняются настройки воркспейса (текущего проекта).
Вернитесь к открытому файлу test.py и запустите его на исполнение через команду из контекстного меню Run code:



В выводе интегрированного терминала вы увидите информацию в которой можно убедится, что код исполнялся на удаленной машине (контейнере). Теперь, всякий раз, когда нужно запустить проект в контейнере, достаточно внести настройки в WORKSPACE_SETTINGS или создать файл .vscode/settings.json относительно каждого проекта и затем немного подправить пути в настройках.

Удаленный Debug в VS Code


Раскомментируйте строчки в файле test.py и сохраните этот файл:

#### BEGIN. Visual Studio remote debugging server for Python
import ptvsd

print('Waiting connection on port 3000')
ptvsd.enable_attach('my_secret', address=('0.0.0.0', 3000))
ptvsd.wait_for_attach()
ptvsd.break_into_debugger()
#### END. Visual Studio remote debugging server for Python

Всякий раз, когда есть необходимость удаленного дебага, эти строчки кода необходимо вставить в тот файл, который запускается на исполнение. Модуль ptvsd на стороне контейнера открывает порт 3000 и ожидает подключение внешнего дебаггера. При желании, вы можете сконфигурировать ptvsd на свой лад.

Запустите код, вызвав контекстное меню Run Code. В интегрированном терминале вы увидите сообщение, что скрипт ожидает подключения на порту 3000. Перейдите в дебаггер, и создайте новую конфигурацию для этого проекта:



У вас откроется файл launch.json на редактирование в котором уже будут предустановки. Внесите в него следующий блок кода:

        {
            "name": "Python LXC",
            "type": "python",
            "request": "attach",
            "localRoot": "${workspaceRoot}",
            "remoteRoot": "/mnt/hostfs/pytest",
            "host": "pytest",
            "port": 3000,
            "secret": "my_secret"
        }

Сохраните его, он появится в каталоге .vscode/launch.json рядом с файлом setting.json. Затем, выберите слева в низподающем списке Python LXC, это та конфигурация дебаггера, которую мы только что внесли:



Нажмите Start Debugging. В окне с файлом test.py вы увидите работающий дебагер и подтверждение, что код запущен удаленно:



На этом всё. Настройка завершена. Всё что нужно в дальнейшем для нового проекта python, это сконфигурировать два конфиг-файла .vscode/settings.json и .vscode/launche.json. Если у вас есть вопросы по теме, прошу задавать в комментариях. Спасибо за внимание, успешных вам проектов!

UPD от 10.11.2017 21:35 MSK.
В комментариях, пользователь SirEdvin задал вопрос о возможности автодополнения (или IntelliSense в терминах Microsoft) в редакторе тех python-модулей, которые установлены только в контейнере, т.е., этих модулей нет на локальной машине на которой запускается VS Code.
В статье этот момент был упущен, поэтому с радостью дополняю небольшой главою:

Настройка автодополнения (IntelliSense) для внешних python-библиотек


Например, в вашем проекте используется модуль, который не поставляется в коробке по умолчанию с python, и вы этот модуль установили только в контейнере. Как же сделать так, чтобы VS Code «понимал», автодополнял, выводил подсказки в редакторе при этом не устанавливая модуль на хосте, а получал из модуля в контейнере?
Для решения этого вопроса, откройте настройки WORKSPACE_SETTINGS по адресу: ВАШ_ПУТЬ_К_ПРОЕКТУ/.vscode/settings.json и добавьте параметр python.autoComplete.extraPaths, а в качестве ключей укажите список каталогов контейнера в которых находятся python модули.

Например, наш контейнер доступен в файловой системе хоста по адресу: /var/lib/lxd/containers/pytest/rootfs/. Это корневая файловая система контейнера pytest. Зная, где Python устанавливает внешние библиотеки, мы с легкостью можем добавить этот путь, чтобы получить полный путь к внешним библиотекам. Для таких модулей как numpy полный путь будет: /var/lib/lxd/containers/pytest/rootfs/usr/local/lib/python3.6/dist-packages. Этот путь мы добавим в настройку python.autoComplete.extraPaths, вот так:



Сохраните настройки и вернитесь к вашему проекту. Попробуйте импортировать модуль numpy и вызвать автодополнение клавишей CTRL + SPACE. У вас должно получиться так как на экране:



Если сразу не получается, не расстраивайтесь, VS Code кеширует результаты и это сразу может не заработать, попробуйте закрыть-открыть редактор, поиграться с клавишами CTRL+SPACE после ключевого слова from… import.

На этом всё :)




К сожалению, не доступен сервер mySQL