С нуля до Junior DevOps в 2026 году. Часть 7.2. Ansible в DevOps: Docker, Kubernetes, CI/CD и Terraform

от автора

В предыдущих статьях мы познакомились с основами 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.10web-02 → 10.10.1.11web-03 → 10.10.1.12

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


Dynamic Inventory

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

[web]10.10.1.1010.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 Node192.168.56.1Managed Node192.168.56.101

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


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

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

mkdir ansible-docker-democd 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 .venvsource .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.ymlroles_path = roleshost_key_checking = Falseinterpreter_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-cedocker-ce-clicontainerd.iodocker-buildx-plugindocker-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: deployapp_group: deployapp_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"

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

webredispostgres

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

ssh ubuntu@192.168.56.101

И проверить:

cd /opt/myappdocker 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 userapplication directorycompose.yml

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

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

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

Создадим:

molecule/docker_app/molecule.yml
---driver:  name: dockerplatforms:  - name: instance    image: ubuntu:24.04    pre_build_image: trueprovisioner:  name: ansibleverifier:  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▼userdirectorycomposedeployment

а Molecule:

Molecule▼userdirectorycompose

То есть 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▼MoleculeIntegration▼Ubuntu VMProduction▼Real servers

Добавляем Git

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

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

Создаём .gitignore:

.venv/__pycache__/*.retry.molecule/

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


GitHub Actions

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

.github/workflows/ansible.yml
name: Ansible CIon:  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, сбор метрик и логов, и т.д., а также построение полноценной системы мониторинга инфраструктуры.

ссылка на оригинал статьи https://habr.com/ru/articles/1071014/