Начало...

Все, кто пишет Ansible-роли, рано или поздно сталкиваются с необходимостью быстрых локальных тестов, проверить работу роли на раннем этапе разработки. Для этого существует инструмент Molecule — он управляет жизненным циклом тестовых инстансов, запускает роль, проверяет идемпотентность и в��полняет тесты. Molecule сам по себе не создаёт виртуальные машины — он использует драйверы, которые создают инстансы (локально или в облаке) и предоставляют Molecule доступ по SSH.

Почему macOS Apple Silicon — отдельная "боль"?

Пользователи Mac с чипами Apple Silicon (M1, M2, M3, M4) часто сталкиваются с ограничениями традиционных локальных провайдеров виртуализации.

Например, VirtualBox 7.2 действительно имеет поддержку Apple Silicon но пока только экспериментальную, и связка Vagrant + VirtualBox в целом запускается, но работает неэффективно. При выделении 1 ГБ оперативной памяти для гостевой системы VirtualBox фактически “съедает” около 1.6 ГБ из оперативки хоста — т.е. примерно на 60 % больше заявленного.

Попытка запустить Molecule в связке Molecule → Vagrant → VirtualBox у меня так и не увенчалась успехом. Процесс падает с ошибкой:

[ERROR]: couldn't resolve module/action 'vagrant'. 
This often indicates a misspelling, missing collection, or incorrect module path.

Вероятно, причина в том, что модуль vagrant был удалён из коллекции community.general, а проект community.vagrant так и остался в состоянии «Initial commit» с версией 0.0.0 — то есть фактически заброшен.

Связка Molecule + Vagrant + VMware формально работает, но требует множества ручных настроек, плагинов и лицензий — в общем, типичные "танцы с бубном". В этой статье подробно описаны обходные пути и хаки, но, на мой взгляд, они слишком громоздки для повседневной разработки, но вполне рабочий вариант. Лично я больше стремлюсь к нативной виртуализации без "костылей", все таки они заметно снижают производительность и надежность.

После появления QEMU for macOS и поддержки ARM-архитектуры можно попробовать связку Vagrant + QEMU. У меня заработало. Но так же как и в Virtualbox при выделении 1 ГБ оперативной памяти для гостевой системы фактически “съедает” около 1.4 ГБ из оперативки хоста. QEMU умеет эмулировать разные архитектуры. И на первый взгляд, QEMU кажется универсальным решением: поддерживает ARM, доступен через Homebrew, умеет cloud-init и позволяет Molecule запускать полноценные образы. Однако на практике всё оказывается гораздо сложнее.

Во-первых, на macOS Apple Silicon QEMU не может использовать классический tun/tap или bridge — для сетевого режима vmnet-shared требуется root-доступ. Molecule автоматически поднимает виртуалки через sudo, и если не настроен NOPASSWD, выполнение обрывается ошибкой:

[ERROR]: Task failed: Premature end of stream waiting for become success.
sudo: a password is required
Premature end of stream waiting for become success.

Проблема в том, что даже добавление правил в /etc/sudoers часто не решает ситуацию, поскольку Ansible выполняет команды в неинтерактивном окружении, а sudo на macOS дополнительно защищён системным SIP (System Integrity Protection).

Во-вторых, QEMU требует утилиту mkisofs (или genisoimage) для генерации cloud-init ISO. На macOS её нет по умолчанию, Homebrew не всегда добавляет бинарь в $PATH, и таска падает с ошибкой:

Error executing command: [Errno 2] No such file or directory: 'mkisofs'

В итоге связка Molecule + QEMU на macOS превращается в набор обходных решений, ручных правок sudoers, странных флагов и долгого ожидания запуска виртуалок. Это всё делает процесс тестирования Ansible-ролей не автоматизированным, а экспериментальным. У меня так и не запустилась связка molecule-qemu.

Еще можно использовать драйвер Docker — Molecule прекрасно работает с контейнерами и позволяет запускать роли в изолированных окружениях. Однако у этого подхода есть важное ограничение: контейнеры не поддерживают полноценный systemd.

Если роль использует systemd-юниты, управляет сервисами через ansible.builtin.service, изменяет конфигурации в /etc/systemd/system — внутри Docker это просто не будет работать. Можно попытаться использовать образы с «поддельным systemd» (например, jrei/systemd-ubuntu), но это лишь частично решает проблему и часто приводит к нестабильным тестам.

Поэтому для большинства «реальных» ролей, особенно тех, что настраивают сервисы уровня ОС (Nginx, Prometheus, Consul, и т.д.), требуется полноценная виртуальная машина с настоящим systemd — а значит, Docker-драйвер не подходит.

Lima: Современная альтернатива Vagrant для macOS

Что такое Lima?

Lima (Linux Machines) — это легковесный кроссплатформенный инструмент командной строки для запуска полноценных Linux виртуальных машин на macOS и Linux. Lima работает на базе QEMU, использует простые YAML файлы для конфигурации и полностью совместим с Apple Silicon.

Выше в статье я рассмотрел использование Vagrant с различными провайдерами и отметил их недостатки. Теперь перейдём к преимуществам Lima, которая во многих случаях становится более удобной и современной альтернативой.

Нативная поддержка Apple Silicon
Lima работает через встроенный в macOS Hypervisor.framework, обеспечивая полную поддержку архитектуры ARM64 без эмуляции. Образы Ubuntu, Debian и других дистрибутивов запускаются напрямую, быстро и без накладных расходов на виртуализацию.

Кроссплатформенность
Хотя Lima создавалась как альтернатива Docker Desktop для macOS, она прекрасно работает и под Linux с использованием QEMU, что делает её удобным инструментом для разработчиков, работающих на разных платформах.

Декларативные ВМ “как код”
Все параметры виртуальной машины описываются в YAML-файле — от количества CPU до сетевых интерфейсов и примонтированных директорий. Один конфиг можно хранить в репозитории и воспроизводить окружения одинаково у всей команды.

Автоматический проброс папок
Каталоги из хоста автоматически монтируются внутрь ВМ (/Users, /Volumes, /tmp и т. д.) без ручной настройки 9p, sshfs или virtio-fs, что особенно удобно для Molecule — роли и зависимости доступны сразу.

Быстрый SSH-доступ
Каждая Lima-машина автоматически получает SSH-ключ и конфиг, доступ к ВМ выполняется простой командой

limactl shell <vm-name>

или напрямую через ssh без поиска IP-адреса и паролей.

Поддержка Ansible из коробки
Lima может использовать Ansible для провижининга ВМ при создании, а в связке с драйвером molecule-lima она становится полноценным тестовым бэкендом для Ansible-ролей — локально, быстро и без root-прав.

Поддержка Docker без Docker Desktop
Lima может запускать Docker или containerd внутри гостевой ВМ, предоставляя локальный контейнерный runtime без лицензий и ограничений Docker Desktop.

Установка Lima

macOS:

brew install lima

Linux:

# Установка QEMU
sudo apt update && sudo apt install -y qemu-utils qemu-kvm qemu-system-x86

# Установка Lima
VERSION=$(curl -fsSL https://api.github.com/repos/lima-vm/lima/releases/latest | jq -r .tag_name)
curl -fsSL "https://github.com/lima-vm/lima/releases/download/${VERSION}/lima-${VERSION:1}-$(uname -s)-$(uname -m).tar.gz" | sudo tar Cxzvm /usr/local

Теперь создать полностью настроенную Ubuntu VM можно за секунды:

# Запуск из предустановленного шаблона
limactl start ubuntu-lts
lima ubuntu-lts

# Или запуск из собственного YAML файла
limactl start ./vm-test.yaml

Сравнение конфигурации: Vagrant vs Lima

Vagrant (Ruby DSL)

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.synced_folder ".", "/vagrant"
  
  config.vm.provider "virtualbox" do |vb|
    vb.memory = 2048
    vb.cpus = 2
  end
  
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y cowsay
  SHELL
end

Lima (YAML)

# file: vm-test.yaml
images:
  - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
    arch: "aarch64"  # или "x86_64" для Intel/AMD

cpus: 2
memory: "2GiB"
disk: "20GiB"

# Настройка сети 
networks:
  - lima: bridged

# Проброс папок
mounts:
  - location: "."
    writable: true
    mountPoint: "/home/ubuntu/workspace"

# SSH порт
ssh:
  localPort: 60022

# Проброс портов
portForwards:
  - guestPort: 80
    hostPort: 8080

# Провижининг через shell
provision:
  - mode: system
    script: |
      #!/bin/bash
      apt update
      apt install -y cowsay ansible curl

Запуск:

limactl start ./myvm.yaml

Основные команды Lima

# Запустить ВМ
limactl start vm-test.yaml
limactl start --name="custom-name-vm" vm-test.yaml

# SSH в ВМ
limactl shell vm-test

# Остановить ВМ
limactl stop vm-test

# Перезапустить ВМ
limactl stop vm-test && limactl start vm-test

# Удалить ВМ
limactl delete vm-test

# Список всех ВМ
limactl list

# Показать SSH конфигурацию
limactl show-ssh vm-test

Провижининг с Ansible

Lima поддерживает два способа провижининга через Ansible:

1. Внутри ВМ (проще и надежнее)

provision:
  - mode: system
    script: |
      apt update
      apt install -y ansible
      cd /home/ubuntu/workspace
      ansible-playbook playbook.yml -i localhost, --connection=local

2. С хоста (требует Ansible на хосте)

provision:
  - mode: host
    script: |
      # Ждем готовности SSH
      for i in $(seq 1 30); do
        if ssh -F ~/.lima/vm-test/ssh.config default 'echo ready'; then
          break
        fi
        sleep 2
      done
      
      # Запускаем Ansible с хоста
      ansible-playbook -i ~/.lima/vm-test/ssh.config playbook.yml

Где искать образы для Lima

Lima использует cloud-init ready образы. Нет централизованного репозитория (как Vagrant Cloud), можно использовать в принципе любой образ поддерживающий cloud-init:

Ubuntu ARM64:

images:
  - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
    arch: "aarch64"

Debian x86_64:

images:
  - location: "https://cloud.debian.org/images/cloud/bullseye/20240311-1467/debian-11-genericcloud-amd64.qcow2"
    arch: "x86_64"

Fedora ARM64:

images:
  - location: "https://download.fedoraproject.org/pub/fedora/linux/releases/39/Cloud/aarch64/images/Fedora-Cloud-Base-39-1.5.aarch64.raw.xz"
    arch: "aarch64"

Требования к образам:

  • Форматы: .img, .qcow2, или .raw

  • Обязательна поддержка cloud-init

  • Используйте cloud server images, а не desktop ISO

Lima — это современная, легковесная альтернатива Vagrant, которая обеспечивает простоту и нативную производительность. С декларативными YAML конфигурациями, встроенным провижинингом Ansible и поддержкой Intel, AMD и Apple Silicon.

Molecule-lima: Интеграция Lima с Molecule

О проекте

Открыв для себя Lima, я понял, что это идеальное решение для локального тестирования Ansible ролей на Apple Silicon. Вдохновившись статьей "Пишем свой драйвер Molecule без костылей и боли", я решил написать полноценный драйвер для Molecule.

molecule-lima — это драйвер, который интегрирует Lima в экосистему Molecule, предоставляя:

  • Полную совместимость со стандартным Molecule workflow

  • Декларативную конфигурацию инстансов через molecule.yml

  • Автоматическое управление жизненным циклом Lima VM

  • Прозрачную интеграцию с Ansible через SSH

  • Поддержку множественных платформ и параллельного тестирования

Драйвер полностью реализован на Ansible playbooks, что делает его прозрачным, расширяемым и легким для понимания. Это beta-версия, но она уже полностью рабочая. Исходный код лежит здесь Все настройки инстансов и зависимости смотрите в репозитории.

Установка molecule-lima

Подготовим окружение. И активируем его:

# Создать окружение 
python3 -m venv venv

# Активировать его
source venv/bin/activate

# Обновим pip
python3 -m pip install --upgrade pip

# Установим Ansible и Molecule
pip install ansible molecule
#

Драйвер доступен в PyPI:

pip install molecule-lima

Так же можно установить из исходников:

# Клонировать репозиторий
git clone https://github.com/filatof/molecule-lima.git
cd molecule-lima

# Установить 
pip install -e .

Возможно кому то понадобится установить коллекцию community.general, если не установлена глобально в ~/.ansible/collections :

ansible-galaxy collection install -r requirements.yml 

Теперь все готово для локального тестирования разрабатываемой Ansible роли. Для опыта можно взять мою роль ansible-role-docker

Для начала создадим сценарий molecule командой:

molecule init scenario 

По умолчанию будет создан сценарий с именем default, однако при желании ��ожно задать своё имя, например lima:

molecule init scenario lima

В этом случае при запуске команд molecule (например, molecule create, molecule converge и т.д.) необходимо будет явно указывать имя сценария, добавляя флаг -s, например:

molecule create -s lima

После инициализации сценария default Molecule создаёт базовую структуру файлов:

(venv) fill@MacBookAir ~/Documents/ansible_role/ansible-role-docker ➜ ls -l molecule/default
total 40
-rw-r--r--@ 1 fill  staff   372 13 нояб. 17:39 converge.yml
-rw-r--r--@ 1 fill  staff  1182 13 нояб. 17:39 create.yml
-rw-r--r--@ 1 fill  staff   622 13 нояб. 17:39 destroy.yml
-rw-r--r--@ 1 fill  staff  1170 13 нояб. 17:39 molecule.yml
-rw-r--r--@ 1 fill  staff   312 13 нояб. 17:39 verify.yml

Начиная с версии Molecule 6.0+, параметр --driver-name для команды molecule init scenario был удалён. Теперь Molecule использует только delegated driver (также называемый “default”), а все остальные драйверы — Docker, Podman, LXD, Proxmox и др. — подключаются как плагины через этот драйвер. Драйвер molecule-lima также реализует этот механизм — он наследуется от molecule.api.Driver и подключается как плагин через систему entry_points. Таким образом, Molecule взаимодействует с ним так же, как с любым другим драйвером (Docker, Podman и т.д.), просто загружая его по имени molecule-lima, указанному в molecule.yml.

Таким образом, команда molecule init scenario создаёт лишь базовый набор файлов без привязки к конкретному драйверу, а выбор нужного драйвера осуществляется вручную в файле molecule.yml.

Чтобы подготовить сценарий для собственного драйвера, удалим ненужные файлы, созданные по умолчанию:

rm molecule/default/create.yml
rm molecule/default/destroy.yml

Файл molecule.yml заменим следующим:

---
dependency:
  name: galaxy

driver:
  name: molecule-lima
  ssh_timeout: 120

platforms:
  - name: instance-ubuntu
    image: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
    arch: aarch64
    vm_type: vz
    cpus: 2
    memory: 2GiB
    disk: 20GiB
    python_interpreter: /usr/bin/python3

provisioner:
  name: ansible
  config_options:
    defaults:
      callbacks_enabled: profile_tasks,timer
      stdout_callback: yaml
      host_key_checking: false

verifier:
  name: ansible

scenario:
  test_sequence:
    - dependency
    - cleanup
    - destroy
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - side_effect
    - verify
    - cleanup
    - destroy

Файл converge.yml заменим следующим:

---
- name: Converge
  hosts: all
  gather_facts: true
  become: true

  pre_tasks:
    - name: Display connection info
      ansible.builtin.debug:
        msg:
          - "Connected to: {{ inventory_hostname }}"
          - "Host: {{ ansible_host }}:{{ ansible_port }}"
  roles:
    - role: "../../.."

Файл verify.yml заменим следующим:

---
- name: Verify
  hosts: all
  gather_facts: false
  become: true

  tasks:
    - name: Check if Docker is installed
      ansible.builtin.command: docker --version
      register: docker_version
      changed_when: false

    - name: Display Docker version
      ansible.builtin.debug:
        var: docker_version.stdout

    - name: Check if Docker service is running
      ansible.builtin.systemd:
        name: docker
      register: docker_service
      failed_when: false

    - name: Verify Docker service is active
      ansible.builtin.assert:
        that:
          - docker_service.status.ActiveState == "active"
        fail_msg: "Docker service is not active"
        success_msg: "Docker service is running"

    - name: Test Docker run
      ansible.builtin.command: docker run --rm hello-world
      register: docker_test
      changed_when: false

    - name: Verify Docker run output
      ansible.builtin.assert:
        that:
          - "'Hello from Docker!' in docker_test.stdout"
        fail_msg: "Docker test container failed"
        success_msg: "Docker is working correctly"

    - name: Display verification result
      ansible.builtin.debug:
        msg: "All Docker verification tests passed!"

Сейчас можно выполнить:

molecule create

Результат вывода работы плейбука:

Запускаем установку роли:

molecule converge

Результат:

Запускаем тесты:

molecule verify

Результат:

Посмотреть инстансы можно так:

molecule list

Зайти на инстанс можно так:

molecule login -h instance-ubuntu

Удаляем инстанс:

molecule destroy

Заключение

Molecule Lima решает актуальную проблему локального тестирования Ansible ролей на macOS Apple Silicon, избавляя от "танцев с бубном" вокруг Vagrant.

Что получаем:

  • Нативная виртуализация через Virtualization.framework — максимальная производительность

  • Простота установки — pip install molecule-lima и готово

  • Привычный Molecule workflow — все стандартные команды работают

  • Прозрачная архитектура на Ansible playbooks — легко понять и расширить

  • Кроссплатформенность — macOS (ARM/Intel) и Linux

Драйвер находится в beta-версии, но уже полностью функционален и протестирован. Код открыт, документирован и доступен для улучшений сообществом.

Попробуйте: https://github.com/filatof/molecule-lima

Если вы разрабатываете Ansible роли на Apple Silicon — molecule-lima изб��вит вас от головной боли с локальным тестированием.

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