В предыдущих статьях мы познакомились с основами Ansible, создали Inventory, написали первые Playbook и научились использовать Roles, Templates, Variables и Ansible Vault. Теперь пришло время посмотреть на Ansible с другой стороны.В реальных проектах Ansible редко существует изолированно. Он может работать вместе с Terraform, Docker, Kubernetes, GitHub Actions, GitLab CI или Jenkins.

Например, Terraform создаёт виртуальные машины и сети, Ansible устанавливает на них необходимое программное обеспечение, Docker запускает контейнеры, а CI/CD автоматически выполняет весь процесс после изменения кода. В этой статье объединим изученные инструменты в единую систему и разберём, где именно находится Ansible в современной DevOps-инфраструктуре.


Terraform + Ansible: два инструмента — две задачи

Terraform
▼
Создание инфраструктуры
├── Network
├── Virtual Machine
├── Disk
└── Load Balancer
│
▼
Ansible
├── Users
├── Packages
├── Docker
├── Nginx
├── Configuration
└── Application

Terraform отвечает преимущественно за Infrastructure as Code, а Ansible — за Configuration Management. Например, Terraform может создать виртуальную машину:

resource "..." "web" {
  # параметры виртуальной машины
}

После её создания Ansible может подключиться к серверу и установить Docker:

- name: Install Docker
  hosts: web
  become: true

  tasks:
    - name: Install Docker
      ansible.builtin.apt:
        name: docker.io
        state: present

Получается последовательность:

Terraform → Infrastructure → Ansible → Configuration

Terraform → Ansible

На практике между двумя инструментами необходимо передать информацию о созданных серверах. Например, Terraform создал:

web-01 → 10.10.1.10
web-02 → 10.10.1.11
web-03 → 10.10.1.12

Ansible должен получить эти адреса и понять, к каким серверам подключаться. Для небольших проектов можно сформировать обычный Inventory вручную. Однако в динамической инфраструктуре такой подход быстро становится неудобным. Если Terraform каждый день создаёт и удаляет серверы, поддерживать список IP-адресов вручную практически невозможно. Здесь появляется Dynamic Inventory.


Dynamic Inventory

Обычный Inventory выглядит примерно так:

[web]
10.10.1.10
10.10.1.11
[database]
10.10.2.10

Но инфраструктура может измениться:

10.10.1.10 → удалён
10.10.1.11 → удалён
10.10.1.20 → создан
10.10.1.21 → создан

Если Inventory не обновить, Ansible будет использовать устаревшую информацию. Dynamic Inventory позволяет получать список серверов непосредственно из внешнего источника. Например:

Cloud Provider
▼
Dynamic Inventory
▼
Ansible

В зависимости от инфраструктуры источником могут быть:

  • AWS;

  • Azure;

  • Google Cloud;

  • Kubernetes;

  • VMware;

  • другие системы.

Это особенно полезно в больших и постоянно изменяющихся инфраструктурах.


Provisioners: почему их используют осторожно

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

provisioner "remote-exec" {
  inline = [
    "sudo apt update",
    "sudo apt install nginx -y"  ]
}

На первый взгляд это удобно: Terraform создаёт сервер и сразу выполняет команды. Но для полноценной настройки серверов такой подход обычно не является лучшим решением. Provisioners:

  • сложнее контролировать;

  • хуже интегрируются с повторным применением конфигурации;

  • могут сделать процесс развёртывания менее предсказуемым;

  • смешивают создание инфраструктуры и её настройку.

Provisioners при этом могут использоваться для отдельных сценариев, но не должны автоматически становиться основным способом конфигурации серверов.


Terraform → Ansible

На практике существует несколько способов передать информацию от Terraform к Ansible. Самый простой вариант — Terraform создаёт сервер, а его IP-адрес используется в Inventory. Например:

Terraform
▼
192.168.56.101
▼
Ansible Inventory
▼
Ansible Playbook

Для небольшого проекта можно использовать статический Inventory:

all:
  children:
    web:
      hosts:
        web-01:
          ansible_host:
          192.168.56.101

В больших инфраструктурах удобнее использовать Dynamic Inventory.


Практический проект: Ansible + Docker Compose + Molecule + CI/CD

Теперь соберём всё изученное в один небольшой, но уже приближенный к реальному проект. Наша задача — не просто выполнить несколько команд на сервере, а построить воспроизводимый процесс:

Developer
▼
Git
▼
GitHub Actions
├── ansible-lint
├── syntax check
└── Molecule
       ▼
  Ansible Role
       ▼
 Ubuntu Server
       │ 
       ├── Docker Engine
       ├── Docker Compose v2
       ├── deploy
       │
       └── /opt/myapp
            │
            └── Docker Compose
            ├── Nginx
            ├── Redis
            └── PostgreSQL

В результате мы получим проект, который можно:

  • применить к серверу через Ansible;

  • повторно запустить без лишних изменений;

  • проверить --check;

  • проверить через ansible-lint;

  • протестировать роль через Molecule;

  • запускать автоматические проверки через GitHub Actions.

При этом мы разделим тестирование роли и реальный деплой. Molecule будет проверять логику роли в изолированном Docker-окружении, а полноценный Docker Compose deployment — на отдельной Ubuntu-машине. Такой подход позволяет не пытаться устанавливать Docker daemon внутри тестового контейнера.

Что понадобится

Для практики понадобятся:

  • Control Node с Linux;

  • Ansible;

  • Git;

  • Docker на Control Node — он потребуется Molecule;

  • Ubuntu Managed Node;

  • SSH-доступ к Managed Node;

  • GitHub-репозиторий.

В качестве Managed Node можно использовать физический сервер, облачную VM или локальную виртуальную машину VirtualBox.

В примере предположим:

Control Node
192.168.56.1
Managed Node
192.168.56.101

IP-адреса можно заменить на свои.


Создаём проект

Создадим каталог:

mkdir ansible-docker-demo
cd ansible-docker-demo

Создадим базовую структуру:

ansible-docker-demo/
├── ansible.cfg
├── requirements.yml
├── inventory/
│   └── hosts.yml
├── playbooks/
│   └── site.yml
├── roles/
│   └── docker_app/
│       ├── defaults/
│       │   └── main.yml
│       ├── handlers/
│       │   └── main.yml
│       ├── tasks/
│       │   ├── main.yml
│       │   └── deploy.yml
│       ├── templates/
│       │   └── compose.yml.j2
│       └── meta/
│           └── main.yml
├── molecule/
│   └── docker_app/
│       ├── converge.yml
│       ├── molecule.yml
│       └── verify.yml
└── .github/
    └── workflows/
      └── ansible.yml

Здесь впервые используем полноценную Role.

Ansible Role позволяет вынести повторяемую конфигурацию в стандартную структуру каталогов и затем подключать её из разных Playbook. Ansible рекомендует использовать Roles для организации переиспользуемой автоматизации.


Устанавливаем зависимости

На Control Node создадим виртуальное окружение Python:

python3 -m venv .venv
source .venv/bin/activate

Устанавливаем инструменты:

pip install \
  ansible-core \
  ansible-lint \
  molecule \
  molecule-plugins[docker]

ansible-lint используется для статического анализа Ansible-кода, а Molecule — для тестирования Roles и Playbook. Ansible относит оба инструмента к основным средствам проверки Ansible-кода. Теперь создадим requirements.yml:

---
collections:
  - name: community.docker

Устанавливаем коллекцию:

ansible-galaxy collection install -r requirements.yml

Коллекция community.docker не входит в ansible-core. Она устанавливается отдельно и содержит, в частности, community.docker.docker_compose_v2.


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

Создадим ansible.cfg:

[defaults]
inventory = inventory/hosts.yml
roles_path = roles
host_key_checking = False
interpreter_python = auto_silent

Параметр roles_path говорит Ansible, где искать локальные Roles. Теперь Inventory.

inventory/hosts.yml

---
all:
  children:
    web:
      hosts:
        web-01:
          ansible_host: 192.168.56.101
          ansible_user: ubuntu

Проверим структуру:

ansible-inventory --graph

Получим примерно:

@all:
  |--@ungrouped:
  |--@web:
  |    |--web-01

Проверяем SSH

До запуска Playbook сначала проверим обычное SSH-подключение:

ssh ubuntu@192.168.56.101

Если оно работает, проверяем Ansible:

ansible web -m ansible.builtin.ping

Результат:

web-01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Здесь ping — не ICMP ping. Модуль проверяет, способен ли Ansible подключиться к серверу и выполнить действие.


Устанавливаем Docker Engine и Compose v2

Теперь подготовим Managed Node. Docker рекомендует устанавливать Docker Engine на Ubuntu через официальный APT-репозиторий. В состав установки входят Docker Engine, Docker CLI, containerd.io, Buildx и Docker Compose plugin. Важный момент: не будем использовать Ubuntu-пакет docker.io и отдельно надеяться на наличие Compose.

Нам нужны:

docker-ce
docker-ce-cli
containerd.io
docker-buildx-plugin
docker-compose-plugin

Создадим playbooks/site.yml:

---
- name: Prepare Docker host
  hosts: web
  become: true

  tasks:

    - name: Install prerequisites
      ansible.builtin.apt:
        name:
          - ca-certificates
          - curl
        state: present
        update_cache: true

    - name: Create Docker keyrings directory
      ansible.builtin.file:
        path: /etc/apt/keyrings
        state: directory
        mode: "0755"

    - name: Download Docker GPG key
      ansible.builtin.get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: "0644"

    - name: Configure Docker repository
      ansible.builtin.copy:
        dest: /etc/apt/sources.list.d/docker.sources
        mode: "0644"
        content: |
          Types: deb
          URIs: https://download.docker.com/linux/ubuntu
          Suites: {{ ansible_facts.distribution_release }}
          Components: stable
          Architectures: {{ ansible_facts.architecture }}
          Signed-By: /etc/apt/keyrings/docker.asc

    - name: Install Docker
      ansible.builtin.apt:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
        state: present
        update_cache: true

    - name: Start Docker
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: true

Запускаем:

ansible-playbook playbooks/site.yml

Проверяем Docker:

ansible web \
  -m ansible.builtin.command \
  -a "docker --version"

И Compose:

ansible web \
  -m ansible.builtin.command \
  -a "docker compose version"

Docker Compose v2 работает через CLI plugin docker compose. Именно этот plugin требуется модулю community.docker.docker_compose_v2. В актуальной документации минимальное требование для модуля — Docker Compose plugin версии 2.18.0 или новее. Проверяем непосредственно Docker:

ansible web \
  -m ansible.builtin.command \
  -a "docker run --rm hello-world"

Официальная документация Docker также использует hello-world как проверку успешной установки.


Создаём пользователя deploy

Теперь перенесём настройку приложения в Role. Создадим:

roles/docker_app/defaults/main.yml
---
app_user: deploy
app_group: deploy
app_dir: /opt/myapp

Теперь roles/docker_app/tasks/main.yml:

---
- name: Create application group
  ansible.builtin.group:
    name: "{{ app_group }}"
    state: present

- name: Create application user
  ansible.builtin.user:
    name: "{{ app_user }}"
    group: "{{ app_group }}"
    groups: docker
    append: true
    shell: /bin/bash
    create_home: true

- name: Create application directory
  ansible.builtin.file:
    path: "{{ app_dir }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: "0755"

- name: Deploy Compose file
  ansible.builtin.template:
    src: compose.yml.j2
    dest: "{{ app_dir }}/compose.yml"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: "0644"
    notify: Restart application

- name: Deploy application
  ansible.builtin.include_tasks:
    file: deploy.yml

Здесь Ansible:

  1. создаёт deploy;

  2. создаёт каталог;

  3. передаёт Compose;

  4. запускает отдельный набор задач для deployment.

Членство в группе docker удобно для практики, но важно понимать его последствия: доступ к Docker daemon предоставляет пользователю практически уровень привилегий, сопоставимый с root. Поэтому в production модель доступа к Docker необходимо проектировать отдельно.


Создаём Docker Compose

Создадим:

roles/docker_app/templates/compose.yml.j2
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    restart: unless-stopped
  redis:
    image: redis:alpine
    restart: unless-stopped
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: example
      POSTGRES_USER: app
      POSTGRES_PASSWORD: example
      restart: unless-stopped

Для практики этого достаточно. В production пароль PostgreSQL нельзя хранить непосредственно в Git-репозитории. Здесь можно использовать Ansible Vault, внешний Secret Manager или механизм секретов конкретной платформы.


Запускаем Compose через Ansible

Создадим:

roles/docker_app/tasks/deploy.yml
---
- name: Start Docker Compose application
  community.docker.docker_compose_v2:
    project_src: "{{ app_dir }}"
    state: present

Это принципиальный момент. Не используем:

ansible.builtin.command:
  cmd: docker compose up -d

Вместо этого описываем желаемое состояние:

community.docker.docker_compose_v2:
  project_src: /opt/myapp
  state: present

Модуль community.docker.docker_compose_v2 предназначен именно для управления multi-container Docker applications через Docker Compose CLI plugin. В документации также указано, что при повторном запуске с теми же параметрами он может работать идемпотентно.

Старый community.docker.docker_compose предназначался для Compose v1 и удалён из community.docker начиная с версии 4.0.0. Для современных проектов необходимо использовать docker_compose_v2.


Добавляем Handler

Создадим:

roles/docker_app/handlers/main.yml
---
- name: Restart application
  community.docker.docker_compose_v2:
    project_src: "{{ app_dir }}"
    state: restarted

Handler будет вызван только после изменения Compose-файла. Например:

compose.yml
     │
     ▼
Изменился?
  ┌──┴──┐
  │     │
 Да     Нет
  │     │
  ▼     ▼
Restart Ничего

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


Подключаем Role

Теперь playbooks/site.yml дополняем:

---
- name: Prepare Docker host
  hosts: web
  become: true

  tasks:

    - name: Install prerequisites
      ansible.builtin.apt:
        name:
          - ca-certificates
          - curl
        state: present
        update_cache: true

    - name: Create Docker keyrings directory
      ansible.builtin.file:
        path: /etc/apt/keyrings
        state: directory
        mode: "0755"

    - name: Download Docker GPG key
      ansible.builtin.get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: "0644"

    - name: Configure Docker repository
      ansible.builtin.copy:
        dest: /etc/apt/sources.list.d/docker.sources
        mode: "0644"
        content: |
          Types: deb
          URIs: https://download.docker.com/linux/ubuntu
          Suites: {{ ansible_facts.distribution_release }}
          Components: stable
          Architectures: {{ ansible_facts.architecture }}
          Signed-By: /etc/apt/keyrings/docker.asc

    - name: Install Docker
      ansible.builtin.apt:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
        state: present
        update_cache: true

    - name: Start Docker
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: true

    - name: Deploy application
      ansible.builtin.include_role:
        name: docker_app

Теперь весь deployment выполняется одной командой:

ansible-playbook playbooks/site.yml

Проверяем результат

Проверяем контейнеры:

ansible web \
  -m ansible.builtin.command \
  -a "docker compose -f /opt/myapp/compose.yml ps"

Должны появиться:

web
redis
postgres

Можно подключиться к серверу:

ssh ubuntu@192.168.56.101

И проверить:

cd /opt/myapp
docker compose ps

Проверяем Nginx:

curl http://192.168.56.101

Если всё настроено правильно, сервер вернёт HTML стандартной страницы Nginx. Теперь наш deployment уже можно представить так:

Terraform
▼
Ubuntu VM
▼
Ansible
├── Docker
├── deploy
└── /opt/myapp
        │
        ▼
  Docker Compose
   ┌────┼────┐
   ▼    ▼    ▼
Nginx Redis PostgreSQL

Проверяем идемпотентность

Теперь одна из самых важных проверок. Запускаем Playbook ещё раз:

ansible-playbook playbooks/site.yml

Первый запуск должен содержать изменённые задачи:

changed > 0

После второго запуска количество изменений должно значительно уменьшиться, а при отсутствии изменений основной deployment должен завершиться без повторного создания ресурсов. Почему это важно?

Потому что Ansible описывает желаемое состояние:

«пользователь deploy должен существовать»

а не последовательность действий:

«создай пользователя deploy»

То же самое относится к Compose:

state: present

означает, что проект должен находиться в соответствующем состоянии. Документация community.docker.docker_compose_v2 прямо указывает поддержку идемпотентного поведения при повторном запуске с теми же параметрами.


Используем Check Mode

Перед применением изменений можно выполнить:

ansible-playbook \
  playbooks/site.yml \
  --check \
  --diff

Получается workflow:

Изменение
▼
--check --diff
▼
Проверка
▼
Review
▼
Применение

--check особенно полезен при работе с production-инфраструктурой.

Однако не стоит воспринимать Check Mode как абсолютную гарантию того, что реальное выполнение будет полностью идентично: поддержка check mode зависит от конкретного модуля и его взаимодействия с внешними системами.


Добавляем ansible-lint

Теперь проверим качество самого Ansible-кода. Запускаем:

ansible-lint .

ansible-lint анализирует Playbook и Roles и выявляет распространённые ошибки, устаревшие конструкции и нарушения рекомендуемых практик. Например, отдельные правила проверяют неоправданное использование shell и ошибки в условиях. Если lint сообщает об ошибках, лучше исправить сам код, а не сразу отключать правила.

Например, вместо:

- name: Restart service
  ansible.builtin.shell:
    systemctl restart nginx

предпочтительнее:

- name: Restart service
  ansible.builtin.service:
    name: nginx
    state: restarted

Именно поэтому в нашем проекте запуск Compose выполняется специализированным модулем community.docker.docker_compose_v2, а не через shell.


Добавляем Molecule

Теперь самое интересное — тестирование Role. Molecule нужен не для проверки самого production-сервера, а для автоматической проверки того, что Role выполняет ожидаемые действия. В нашем случае Role отвечает за:

deploy user
application directory
compose.yml

Установка Docker Engine остаётся ответственностью отдельного Playbook. Такое разделение полезно и архитектурно:

Playbook
├── Docker host preparation
│
└── docker_app Role
    ├── user
    ├── directory
    └── Compose configuration

Конфигурация Molecule

Создадим:

molecule/docker_app/molecule.yml
---
driver:
  name: docker
platforms:
  - name: instance
    image: ubuntu:24.04
    pre_build_image: true
provisioner:
  name: ansible
verifier:
  name: ansible

Здесь Molecule использует Docker как driver. Сам Docker должен быть доступен на Control Node, поскольку тестовая инфраструктура будет запускаться в контейнерах.


Converge

Создадим:

molecule/docker_app/converge.yml
---
- name: Converge
  hosts: all
  become: true

  vars:
    molecule_test: true

  roles:
    - role: docker_app

Но здесь возникает важный вопрос. Role содержит задачу:

community.docker.docker_compose_v2

Для неё на тестовом контейнере потребовались бы Docker CLI и Compose plugin. Поэтому не будем пытаться запускать production deployment внутри обычного Molecule-контейнера. Внесём небольшое изменение в roles/docker_app/tasks/main.yml.

---
- name: Create application user
  ansible.builtin.user:
    name: "{{ app_user }}"
    group: "{{ app_group }}"
    groups: docker
    append: true
    shell: /bin/bash
    create_home: true

- name: Create application directory
  ansible.builtin.file:
    path: "{{ app_dir }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: "0755"

- name: Deploy Compose file
  ansible.builtin.template:
    src: compose.yml.j2
    dest: "{{ app_dir }}/compose.yml"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: "0644"

- name: Deploy application
  ansible.builtin.include_tasks:
    file: deploy.yml
  when: not molecule_test | default(false)

Теперь:

Production
▼
user
directory
compose
deployment

а Molecule:

Molecule
▼
user
directory
compose

То есть Molecule тестирует саму Role, не подменяя production-окружение.


Создаём Verify

Создадим:

molecule/docker_app/verify.yml
---
- name: Verify
  hosts: all
  become: true

  tasks:

    - name: Check deploy user
      ansible.builtin.getent:
        database: passwd
        key: deploy

    - name: Check application directory
      ansible.builtin.stat:
        path: /opt/myapp
      register: app_dir

    - name: Assert application directory exists
      ansible.builtin.assert:
        that:
          - app_dir.stat.exists
          - app_dir.stat.isdir

    - name: Check Compose file
      ansible.builtin.stat:
        path: /opt/myapp/compose.yml
      register: compose_file

    - name: Assert Compose file exists
      ansible.builtin.assert:
        that:
          - compose_file.stat.exists
          - compose_file.stat.isreg

Теперь тест проверяет реальное состояние контейнера:

deploy существует      +
/opt/myapp существует      +
compose.yml существует

Запускаем Molecule

Выполняем:

molecule test

Molecule создаёт тестовое окружение, применяет Role, выполняет проверки и затем уничтожает тестовую инфраструктуру. Упрощённо процесс выглядит так:

Create
▼
Prepare
▼
Converge
▼
Verify
▼
Destroy

Если одна из проверок не проходит, molecule test завершается с ошибкой. Таким образом, у нас появляется автоматическая проверка Role без использования production-сервера.


Что именно тестирует Molecule

Важно понимать границы теста. Molecule здесь проверяет:

Role
├── создаёт пользователя
├── создаёт каталог
└── создаёт Compose-файл

А отдельный integration test на Ubuntu проверяет:

Docker Engine
      +
Docker Compose
      +
Real Compose deployment
      +
    HTTP

Это не недостаток, а разделение уровней тестирования. Получается:

Unit/Role-level
▼
Molecule

Integration
▼
Ubuntu VM

Production
▼
Real servers

Добавляем Git

Теперь сохраним проект в Git.

git init
git add .
git commit -m "Initial Ansible Docker deployment"

Создаём .gitignore:

.venv/
__pycache__/
*.retry
.molecule/

Если в будущем в проекте появятся Vault-файлы, SSH-ключи или локальные секреты, их также нельзя отправлять в публичный репозиторий.


GitHub Actions

Теперь автоматизируем проверки. Создадим:

.github/workflows/ansible.yml
name: Ansible CI

on:
  push:
  pull_request:

jobs:

  lint:
    name: Ansible Lint
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install tools
        run: |
          python -m pip install --upgrade pip
          pip install ansible-core ansible-lint molecule "molecule-plugins[docker]"

      - name: Install Ansible collections
        run: |
          ansible-galaxy collection install -r requirements.yml

      - name: Run ansible-lint
        run: ansible-lint .

  syntax:
    name: Ansible Syntax Check
    runs-on: ubuntu-latest
    needs: lint

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install Ansible
        run: |
          python -m pip install --upgrade pip
          pip install ansible-core

      - name: Install collections
        run: |
          ansible-galaxy collection install -r requirements.yml

      - name: Syntax check
        run: |
          ansible-playbook playbooks/site.yml --syntax-check

  molecule:
    name: Molecule
    runs-on: ubuntu-latest
    needs: syntax

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install tools
        run: |
          python -m pip install --upgrade pip
          pip install ansible-core ansible-lint molecule "molecule-plugins[docker]"

      - name: Install collections
        run: |
          ansible-galaxy collection install -r requirements.yml

      - name: Run Molecule
        run: molecule test

Теперь после каждого push или pull request GitHub Actions выполняет:

GitHub
▼
Checkout
▼
ansible-lint
▼
syntax-check
▼
Molecule

Если хотя бы один этап завершится с ошибкой, workflow будет считаться неуспешным.


Добавляем CD

Пока GitHub Actions выполняет только проверки — это CI (Continuous Integration). Для полноценного CD (Continuous Delivery/Deployment) runner должен иметь сетевой доступ к Managed Node. В нашем учебном примере Managed Node находится в приватной сети. 192.168.56.101 .Поэтому GitHub-hosted runner не сможет автоматически подключиться к нему без дополнительной сетевой инфраструктуры. В production для этого можно использовать, например:

  • self-hosted GitHub Actions runner внутри приватной сети;

  • VPN или другой защищённый сетевой канал;

  • публично доступный сервер с корректно настроенным firewall;

  • отдельную deployment-инфраструктуру.

Поэтому следующий workflow рассматриваем как архитектурный пример CD:

Lint
▼
Syntax
▼
Molecule
▼
Deploy
▼
Production

Например, deployment можно запускать только после успешного прохождения тестов и только для main. Упрощённый job:

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs:
      - lint
      - syntax
      - molecule\
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:

      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install Ansible
        run: |
          python -m pip install --upgrade pip
          pip install ansible-core

      - name: Install collections
        run: |
          ansible-galaxy collection install -r requirements.yml

      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519

      - name: Deploy
        run: |
          ansible-playbook \
            -i inventory/hosts.yml \
            playbooks/site.yml

Но перед использованием такого job в production нужно изменить Inventory и добавить секреты. SSH-ключ, пароль, токены и другие секреты нельзя хранить непосредственно в YAML workflow или Git-репозитории.


Полный pipeline

Теперь наша архитектура выглядит так:

                        GitHub
                           ▼
                      Pull Request
                           ▼
                         Lint
                           ▼
                         Syntax
                           ▼
                        Molecule
                           ▼
                       Code Review
                           ▼
                          Merge
                           ▼
                         Deploy
                           ▼
                        Ansible
                           ▼
                     Ubuntu Server
                ┌──────────┼──────────┐
                ▼          ▼          ▼
             Docker      deploy     Config
                ▼
           Compose v2
         ┌──────┼──────┐
         ▼      ▼      ▼
       Nginx   Redis PostgreSQL

Теперь изменение инфраструктурного кода проходит тот же принцип, что и обычный application code:

Изменение
▼
Pull Request
▼
Lint
▼
Tests
▼
Review
▼
Merge
▼
Deploy

Проверяем весь сценарий вручную

Перед тем как доверять deployment CI/CD, полезно пройти полный цикл на тестовой VM.

Первый запуск

ansible-playbook playbooks/site.yml

Проверяем:

curl http://192.168.56.101

Второй запуск

ansible-playbook playbooks/site.yml

Проверяем отсутствие лишних изменений.

Check Mode

ansible-playbook \
  playbooks/site.yml \
  --check \
  --diff

Lint

ansible-lint .

Syntax Check

ansible-playbook \
  playbooks/site.yml \
  --syntax-check

Molecule

molecule test

После этого уже можно отправлять проект в GitHub:

git add .
git commit -m "Add Docker deployment and CI"
git push

GitHub Actions автоматически выполнит проверки.


Что произошло с точки зрения DevOps

В начале у нас был только сервер. Вручную пришлось бы:

SSH
▼
apt update
▼
Docker
▼
Compose
▼
user
▼
directory
▼
compose.yml
▼
docker compose up

Теперь всё это описано кодом. Ansible приводит инфраструктуру к заданному состоянию:

Ubuntu
├── Docker installed
├── Docker enabled
├── deploy exists
├── /opt/myapp exists
├── compose.yml exists
└── application running

А CI добавляет ещё один уровень:

Infrastructure as Code
▼
Static Analysis
▼
Automated Tests
▼
Code Review
▼
Deployment

Именно здесь становится заметна разница между простым использованием Ansible и инфраструктурой как кодом.


Почему мы не устанавливаем Docker через Role приложения

Можно было поместить абсолютно всё в одну Role:

docker_app
├── Docker
├── user
├── directories
├── Compose
└── deployment

Для учебного проекта это сработало бы. Но такое разделение лучше масштабируется:

Playbook
│
├── Docker host preparation
│
└── docker_app
      ├── user
      ├── directories
      ├── configuration
      └── deployment

Позже можно выделить отдельные Roles:

roles/
├── docker/
├── users/
├── nginx/
└── application/

Тогда одна Role отвечает за Docker, другая — за пользователей, третья — за приложение.


Что можно улучшить в production

Наш проект специально остаётся небольшим. Для production стоило бы:

  • фиксировать версии коллекций;

  • использовать отдельные inventories для окружений;

  • ограничивать права CI/CD;

  • не давать pipeline необязательный доступ к production;

  • использовать секрет-хранилище;

  • проводить deployment через review;

  • разделять инфраструктуру и конфигурацию.


Ресурсы для обучения

1. Официальная документация Ansible

Главный источник для изучения Ansible. Здесь есть Getting Started, документация по Playbook, Inventory, Modules, Roles, Variables и другим компонентам. Документация поддерживает актуальные версии Ansible и отмечает, в какой версии появились новые возможности.

Ansible Documentation


2. Ansible Getting Started

Если официальная документация кажется слишком большой, начать лучше с отдельного Quick Start: установка Ansible → создание проекта → Inventory → первый Playbook.

Start automating with Ansible


3. LabEx

LabEx предлагает интерактивные лабораторные задания по Ansible, где можно изучать инструмент непосредственно через терминал браузера. Это удобно для тех, кто хочет не просто читать про Inventory, Playbook, Modules и Roles, а сразу выполнять команды и проверять результат.

LabEx


4. Ansible for DevOps — Jeff Geerling

Один из наиболее полезных ресурсов именно для DevOps-практики. Книга начинается с базовых понятий, а затем переходит к Playbook, развёртыванию приложений, Docker и работе с инфраструктурой. Автор регулярно обновляет электронную версию; исходный текст книги также доступен открыто.

Ansible for DevOps


5. Ansible Galaxy

Ansible Galaxy — User Guide — официальный каталог, где можно находить, устанавливать и публиковать Ansible Roles и Collections. В документации есть поиск ролей и коллекций, установка конкретных версий, requirements.yml и управление зависимостями.

Ansible Galaxy

Отдельно стоит посмотреть Ansible Galaxy Developer Guide, если читатель захочет создавать и публиковать собственные Roles или Collections.


6. ansible-lint

Ansible Lint — статический анализатор для Ansible Playbooks, Roles и Collections. Он помогает находить распространённые ошибки, соблюдать рекомендуемые практики и поддерживать единый стиль кода.

Ansible Lint


7. Molecule

Molecule — фреймворк для тестирования Ansible Roles, Playbooks и Collections. Он позволяет создавать тестовые сценарии и проверять автоматизацию на контейнерах, виртуальных машинах и других системах, доступных Ansible.

Molecule


Заключение

Ansible занимает важное место между инфраструктурой и приложением. Главная идея, которую стоит вынести из этого блока: DevOps — это не набор отдельных инструментов, а способ автоматизировать весь жизненный цикл инфраструктуры и приложения. Terraform создаёт инфраструктуру, Ansible приводит серверы к нужному состоянию, Docker и Kubernetes запускают приложения, а CI/CD связывает эти процессы и позволяет выполнять их автоматически.

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

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


  1. sergey2212
    17.08.2026 07:34

    Спасибо за ваш труд. Спасибо что показали сам концепт и вариации применения ansible.

    Классная штука по себе скажу. Чаще использую как слепок инфры что бы потом не мучаться а развернуть все с 1 го клика)