Pull to refresh

Ansible-pull и GitLab CI/CD: когда лучше тянуть, чем толкать

Level of difficultyEasy
Reading time26 min
Views7.6K

👋 Привет!

Меня зовут Андрей, я специалист по управлению IT-инфраструктурой с опытом работы с Windows- и Linux-системами.

Современная IT-инфраструктура — это живой организм, состоящий из множества серверов, виртуальных машин и контейнеров, разбросанных по разным дата-центрам и облачным платформам.

Узлами могут быть:

  • Рабочие станции пользователей,

  • Серверы за NAT в серых зонах сети,

  • Хосты в нестабильных сетевых условиях, периодически исчезающие из доступа.

При работе с такой динамичной инфраструктурой можно столкнуться с рядом сложностей.

  • Сети могут быть ненадежными – сервер или рабочая станция может быть временно недоступна из-за сбоя связи, перегрузки или политики безопасности.

  • Узлы за NAT или в изолированных зонах – Ansible Control Node не имеет прямого доступа к таким узлам.

  • Удалённые устройства перемещаются – например, ноутбуки разработчиков или оборудование в мобильных дата-центрах.

Мой телеграмм канал - сообщество, где делятся опытом

https://t.me/IT_Chuyana

Проблемы классической push-модели Ansible

В push-модели центральный сервер Ansible отвечает за доставку конфигураций узлам при их сетевой доступности.

Сервер передает конфигурацию каждому узлу при его доступности
Сервер передает конфигурацию каждому узлу при его доступности

В условиях нестабильных и распределённых сетей реализация такой модели приводит к ряду проблем:

Несинхронизированность конфигурации

  • Если узел был недоступен во время выполнения Ansible-задач (playbook), он остаётся устаревшим и может работать с неверными настройками.

  • Невозможно однозначно определить, какая версия конфигурации сейчас на узле, если он периодически исчезает из сети.

  • Разные версии конфигурации у разных серверов могут вызывать неожиданные баги и рассинхронизацию работы сервисов.

Перегрузка сервера управления

  • При традиционном подходе Ansible один управляющий сервер должен обрабатывать все узлы, что становится узким местом.

  • Масштабирование требует дополнительных ресурсов и усложняет процесс.

  • Если центральный сервер управления выходит из строя, автоматизация останавливается.

Риск конфигурационного дрейфа

  • Без постоянного контроля и обновления параметров узлы могут "дрейфовать" от эталонной конфигурации.

  • Если администратор вручную изменил настройки, а потом Ansible не применялся долгое время – могут возникнуть неожиданные конфликты.

  • В результате часть серверов может оказаться в непредсказуемом состоянии для команды DevOps

Как решить эти проблемы

Оптимальным решением для таких условий становится ansible-pull – механизм, который меняет традиционную модель работы Ansible. Вместо того чтобы «толкать» конфигурацию на удалённые узлы, каждый сервер самостоятельно загружает и применяет конфигурацию.

Оговорюсь сразу: по сути, ansible-pull – это выполнение команды:
git pull && ansible-playbook -i inventory_with...

Она запускается локально, используя код, загруженный из удалённого репозитория.

Однако, в отличие от Puppet, работа с YAML-синтаксисом, на мой взгляд, гораздо удобнее. Кроме того, функциональные возможности Ansible шире, что делает его более гибким и мощным инструментом для автоматизации.

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

Возможности ansible-pull

  • Оборудование подключается нерегулярно — само загружает конфиг после включения.

  • Не нужен централизованный Ansible Control Node (каждый узел отвечает сам за себя).

  • Работает даже за NAT / Firewall — тянет конфиг из облачного репозитория (Git).

  • Обновления распространяются автоматически — нет зависимости от административного доступа.

  • Легко откатывать изменения — реверт через Git происходит глобально.

Примеры применения ansible-pull

  • Распределённые станции электропитания, метеостанции и датчики

  • Геораспределённые системы телефонии (IP-телефоны, Asterisk)

  • Рабочие станции пользователей (ноутбуки, кассовые терминалы)

  • Автоматическая настройка новых серверов (автопровижнинг)

  • IoT-устройства и сетевое оборудование

Доводы приведены, давайте подумаем как нам реализовать этот механизм.

Ход работы

Нашу задачу мы разделим на несколько подзадач:

  1. Подготовка тестового окружения на виртуальных машинах через Vagrant

  2. Настройка узлов к режиму Ansible-pull, выдача им ролей и RSA-ключей

  3. Конфигурирование среды для тестирования качества кода в Docker-контейнере

  4. Настройка CI/CD-пайплайна для автоматизирования процесса развертывания и тестирования в конвейере

  5. Создание ролей для Ansible-pull.

CI/CD-конвейер организуем по схеме:

1. Подготовка тестового окружения на виртуальных машинах через Vagrant

Vagrant это инструмент для быстрого создания и управления виртуальными средами. Он позволяет автоматически поднимать виртуальные машины с заранее заданной конфигурацией.

В нашем случае мы подготовим тестовое окружение из четырёх узлов:

  • webserver (веб-сервер) Debian 12,

  • application (приложение) Debian 12,

  • client-1 Ubuntu 18.04

  • client-2 CentOS 7.

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

В качестве провайдера виртуальных машин мы будем использовать libvirt. Конфигурационный файл Vagrantfile для vagrant/libvirt/ будет содержать необходимые настройки для развертывания данного окружения.

Составляем Vagrantfile:

Скрытый текст
# vagrant\libvirt\Vagrantfile
# Интерфейс для моста
BRIDGE_NET = "192.168.2."
BRIDGE_NAME = "br0"
BRIDGE_DNS = "192.168.2.1"

# Путь к образам виртуальных машин
IMAGE_PATH = "/kvm/images" 

# Домен который будем использовать для всей площадки
DOMAIN = "ch.ap"  # debian 12
BOX_1 = "d12"     # debian 12
BOX_2 = "u1804"   # ubuntu 18.04
BOX_3 = "c7"      # centos 7

# Укажите путь к общей папке на хосте и путь к точке монтирования в ВМ
HOST_SHARED_FOLDER = "."  
VM_SHARED_FOLDER = "/home/vagrant/shared"

# Имена хостов
HOSTNAME_1 = "t-webserver"
HOSTNAME_2 = "t-application"
HOSTNAME_3 = "t-client-1"
HOSTNAME_4 = "t-client-2"

# Массив из хешей, в котором задаются настройки для каждой виртуальной машины.
MACHINES = {
  HOSTNAME_1.to_sym => {
    :box_name     => BOX_1,
    :host_name    => HOSTNAME_1, 
    :ip_brig      => BRIDGE_NET + "211",
    :ip_brig_dns  => BRIDGE_DNS,
    :disk_size    => "20G",
    :int_model_type => "e1000",
    :cpu          => 1,
    :ram          => 512,
    :vnc          => 5971,
    :host_role    => "webserver",
  },
  HOSTNAME_2.to_sym => {
    :box_name     => BOX_1,
    :host_name    => HOSTNAME_2, 
    :ip_brig      => BRIDGE_NET + "212",
    :ip_brig_dns  => BRIDGE_DNS,
    :disk_size    => "20G",
    :int_model_type => "e1000",
    :cpu          => 1,
    :ram          => 1024,
    :vnc          => 5972,
    :host_role    => "application",
  },
  HOSTNAME_3.to_sym => {
    :box_name     => BOX_2,
    :host_name    => HOSTNAME_3, 
    :ip_brig      => BRIDGE_NET + "213",
    :ip_brig_dns  => BRIDGE_DNS,
    :disk_size    => "20G",
    :int_model_type => "e1000",
    :cpu          => 1,
    :ram          => 512,
    :vnc          => 5973,
    :host_role    => "client",
  },
  HOSTNAME_4.to_sym => {
    :box_name     => BOX_3,
    :host_name    => HOSTNAME_4, 
    :ip_brig      => BRIDGE_NET + "214",
    :ip_brig_dns  => BRIDGE_DNS,
    :disk_size    => "20G",
    :int_model_type => "e1000",
    :cpu          => 1,
    :ram          => 512,
    :vnc          => 5974,
    :host_role    => "client",
  },
}

## модуль применения настроек
Vagrant.configure("2") do |config|
  MACHINES.each do |boxname, boxconfig|
    config.vm.define boxname do |box|
      box.vm.box = boxconfig[:box_name]               
      box.vm.hostname = boxconfig[:host_name]       
      # Указываем публичную сеть с параметрами моста
      box.vm.network "public_network", 
                      bridge: BRIDGE_NAME, 
                      dev: BRIDGE_NAME, 
                      type: "bridge", 
                      ip: boxconfig[:ip_brig]

      box.vm.provider "libvirt" do |libvirt|
        libvirt.cpus = boxconfig[:cpu]
        libvirt.memory = boxconfig[:ram]
        libvirt.nic_model_type = boxconfig[:int_model_type]
        libvirt.management_network_name = BRIDGE_NAME
        libvirt.storage_pool_name = "images"
        libvirt.graphics_port = boxconfig[:vnc]
        libvirt.graphics_autoport = false
      end

      # # Настройка синхронизированной папки
      box.vm.synced_folder ".", VM_SHARED_FOLDER, type: "nfs", nfs_version: 4, nfs_udp: false
      # --- rsa ключи
      box.vm.provision "file", source: "#{ENV['HOME']}/.ssh/id_rsa.pub", destination: "/tmp/authorized_keys"
      box.vm.provision "shell", inline: <<-SHELL
      # Определяем дистрибутив
      OS_NAME=$(grep -Eoi 'ubuntu|debian|centos|rhel' /etc/os-release | head -1)

      # 📌 Обновление системы
      if [[ "$OS_NAME" == "Ubuntu" ]] || [[ "$OS_NAME" == "Debian" ]]; then
        sudo apt-get update  
        # sudo apt-get upgrade -y

      elif [[ "$OS_NAME" == "CentOS" ]] || [[ "$OS_NAME" == "rhel" ]]; then
        OS_VERSION=$(grep -oP '(?<=VERSION_ID=")[0-9]+' /etc/os-release)

        # 🔄 Переключение репозиториев на архивные для CentOS 7 и 8
        if [[ "$OS_VERSION" == "7" ]] || [[ "$OS_VERSION" == "8" ]]; then
          echo "🔄 Переключаю CentOS $OS_VERSION на архивный репозиторий..."
          sudo sed -i 's|^mirrorlist=.*|#mirrorlist removed|' /etc/yum.repos.d/CentOS-Base.repo
          sudo sed -i 's|^#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|' /etc/yum.repos.d/CentOS-Base.repo
        fi

        # ✅ Очистка кэша и обновление пакетов
        sudo yum clean all
        sudo yum makecache
        # sudo yum update -y
      fi

      # 📌 RSA ключи
      if [ -f /tmp/authorized_keys ]; then
        cat /tmp/authorized_keys >> /home/vagrant/.ssh/authorized_keys
      fi

      # 📌 Таймзона
      sudo timedatectl set-timezone Europe/Moscow

      # 📌 Ansible факт
      sudo mkdir -p /etc/ansible/facts.d
      echo '{ "host_role": "#{boxconfig[:host_role]}" }' | sudo tee /etc/ansible/facts.d/custom.fact > /dev/null
      sudo chmod 0644 /etc/ansible/facts.d/custom.fact

      echo "✅ Настройка завершена"
      SHELL

      if !boxconfig[:prov].nil?
        box.vm.provision "shell", path: boxconfig[:prov]
      end
    end
  end
end

2. Настройка узлов в режиме Ansible-pull, выдача ролей и RSA-ключей

Ansible playbook для pull-режима будет применять роли к узлам, используя заданные пользовательские факты.

Файл плейбука:

Скрытый текст
# ansible/ansible/infra_ansible_pull.yaml
- name: Apply roles dynamically from inventory
  hosts: "{{ target_hosts | default('localhost') }}"
  gather_facts: true

  tasks:
    - name: Explicitly gather custom facts
      ansible.builtin.setup:
        filter: ansible_local

    - name: Include web role
      ansible.builtin.include_role:
        name: web
      when: ansible_local.custom.host_role is defined and ansible_local.custom.host_role == "webserver"

    - name: Include app role
      ansible.builtin.include_role:
        name: app
      when: ansible_local.custom.host_role is defined and ansible_local.custom.host_role == "application"

    - name: Include client role
      ansible.builtin.include_role:
        name: client
      when: ansible_local.custom.host_role is defined and ansible_local.custom.host_role == "client"

Чтобы узлы понимали, какие роли им применять, создаём пользовательские факты Ansible.

Выглядеть они будут так:

Скрытый текст
# /etc/ansible/facts.d/custom.fact
{
  "host_role": "{{ host_role }}"
}

Настройка доступа к репозиторию:

  • Генерируем единую пару RSA-ключей.

  • Приватный ключ записываем на клиентские узлы.

  • Публичный ключ регистрируем в GitLab для доступа к репозиторию.

Скрытый текст
ssh-keygen -t rsa -C "id_ansible_pull" -f ~/.ssh/id_ansible_pull
cat ~/.ssh/id_ansible_pull.pub

Создание инвентаря клиентских узлов

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

  • список узлов

  • логины, пароли. Они приемлемы для стендового окружения, однако в продуктовой среде используем только RSA-ключи!

    Также рекомендую зашифровать этот файл с помощью Ansible Vault.

Скрытый текст
# ansible\node_setting_to_pull\inventory.yml
all:
  hosts:
    webserver:
      ansible_host: 192.168.2.221
      ansible_user: vagrant
      ansible_password: vagrant
      ansible_become_pass: vagrant
      host_role: webserver

    application:
      ansible_host: 192.168.2.222
      ansible_user: vagrant
      ansible_password: vagrant
      ansible_become_pass: vagrant
      host_role: application

    client_1:
      ansible_host: 192.168.2.223
      ansible_user: vagrant
      ansible_password: vagrant
      ansible_become_pass: vagrant
      host_role: client

    client_2:
      ansible_host: 192.168.2.224
      ansible_user: vagrant
      ansible_password: vagrant
      ansible_become_pass: vagrant
      host_role: client

Чтобы Ansible мог аутентифицироваться через пароль, устанавливаем пакет sshpass на управляющий узел.

Запуск настройки узлов для работы с Ansible-pull

Файл плейбука настройки узлов составлен мною так, чтобы работать с несколькими дистрибутивами, что достигается условиями when: ansible_os_family == "Debian" и when: ansible_os_family == "RedHat"

Основные функции:

✅ Создание пользователя Ansible – добавляет пользователя ansible с root-доступом без пароля.
✅ Установка необходимых пакетов – устанавливает Ansible и Git.
✅ Настройка SSH-доступа к Git-репозиторию – настраивает ключи и Known Hosts для безопасного подключения.
✅ Добавление cron-задания – автоматически запускает ansible-pull дважды в час для синхронизации с репозиторием.
✅ Создание фактической роли хоста – записывает host_role в кастомные facts.
✅ Тестирование настройки – проверяет выполнение ansible-pull и cron-задачи.

Скрытый текст
# ansible\node_setting_to_pull\setup_ansible_pull.yml
---
- name: Setup ansible-pull on Debian, Ubuntu, and CentOS
  hosts: all
  vars:
    run_ansible_user: "ansible"
    git_playbook_path: "ansible/infra_ansible_pull.yml"  
    git_branch: "release" # ветка с которой ansible будет подтягивать код, обычно "release"
    ssh_key_src: "~/.ssh/id_ansible_pull" 
    ssh_key_dest: "/home/{{ run_ansible_user }}/.ssh/id_ansible_pull" 
    git_server: "gitlab.ch.ap"
    git_server_ip: "192.168.2.34"
    git_repo: "git@gitlab.ch.ap:AndreyChuyan/ansible_pull_cicd.git"
  tasks:

    ### 🔹 Создание пользователя ansible
    - block:
        - name: Ensure ansible user exists (Debian-based)
          user:
            name: "{{ run_ansible_user }}"
            shell: /bin/bash
            create_home: yes
            groups: sudo
            append: yes
          become: yes
          when: ansible_facts['os_family'] == "Debian"

        - name: Ensure ansible user exists (RHEL-based)
          user:
            name: "{{ run_ansible_user }}"
            shell: /bin/bash
            create_home: yes
            groups: wheel
            append: yes
          become: yes
          when: ansible_facts['os_family'] == "RedHat"

        - name: Allow ansible user to run ansible-pull without password
          copy:
            dest: "/etc/sudoers.d/ansible"
            content: "{{ run_ansible_user }} ALL=(ALL) NOPASSWD: ALL"
            mode: '0440'
          become: yes
      tags: user_setup

    ### 🔹 Установка пакетов (Ansible и Git)
    - block:
        - name: Install Ansible and Git on Debian/Ubuntu
          apt:
            name: 
              - ansible
              - git
            state: present
            update_cache: yes
          become: yes
          when: ansible_os_family == "Debian"

        - name: Install Ansible and Git on CentOS/RedHat
          package:
            name: 
              - epel-release
              - ansible
              - git
            state: present
          become: yes
          when: ansible_os_family == "RedHat"

    # 🔹 Устанавливаем коллекцию ansible.posix, если она отсутствует
        # - name: Ensure ansible.posix collection is installed
        #   ansible.builtin.command:
        #     cmd: ansible-galaxy collection install ansible.posix --ignore-errors
        #   changed_when: false
      tags: ansible_install

    ### 🔹 Настройка SSH-ключей для доступа к Git
    - block:
        - name: Ensure GitLab server IP is in /etc/hosts
          lineinfile:
            path: /etc/hosts
            line: "{{ git_server_ip }} {{ git_server }}"
            regexp: ".*\\s+{{ git_server }}$"
            state: present
          become: yes
        
        - name: Ensure SSH directory exists for ansible user
          file:
            path: "/home/{{ run_ansible_user }}/.ssh"
            state: directory
            mode: '0700'
            owner: "{{ run_ansible_user }}"
            group: "{{ run_ansible_user }}"
          become: yes

        - name: Copy private SSH key for ansible user
          copy:
            src: "{{ ssh_key_src }}"
            dest: "{{ ssh_key_dest }}"
            owner: "{{ run_ansible_user }}"
            group: "{{ run_ansible_user }}"
            mode: '0600'
          no_log: true
          become: yes

        - name: Add Git server fingerprint to known_hosts
          known_hosts:
            path: "/home/{{ run_ansible_user }}/.ssh/known_hosts"
            name: "{{ git_server }}"
            key: "{{ lookup('pipe', 'ssh-keyscan -H ' + git_server) }}"
          become: yes
          # become_user: "{{ run_ansible_user }}"
      tags: ssh_setup


    ### 🔹 Добавление Cron-задачи для ansible-pull
    - block:
        - name: Calculate random cron minute per host
          set_fact:
            random_minute_offset: "{{ (inventory_hostname | hash('md5') | int(base=16) % 30) | int }}"

        - name: Add ansible-pull cron job
          cron:
            name: "Run ansible-pull"
            minute: "{{ (random_minute_offset | int + 0) % 60 }},{{ (random_minute_offset | int + 30) % 60 }}"
            job: >
              /usr/bin/ansible-pull -U {{ git_repo }}
              -C {{ git_branch }}
              --private-key {{ ssh_key_dest }}
              {{ git_playbook_path }}
              | tee /var/log/ansible-pull.log
            user: "{{ run_ansible_user }}"
          become: yes

        - name: Ensure ansible-pull log file exists
          file:
            path: /var/log/ansible-pull.log
            state: touch
            owner: "{{ run_ansible_user }}"
            group: "{{ run_ansible_user }}"
            mode: '0644'
          become: yes
      tags: cron_setup

    ### 🔹 Установка ролей для хостов
    - block:
        - name: Create directoryfor custom fact
          file:
            path: /etc/ansible/facts.d
            state: directory
            mode: '0755'
          become: true

        - name: Create JSON-file with fact "host_role"
          copy:
            dest: /etc/ansible/facts.d/custom.fact
            content: |
              {
                "host_role": "{{ host_role }}"
              }
            mode: '0644'
          become: true
      tags: host_role


    ### 🔹 Тестирование
    - block:
        - name: Check ansible-pull process
          shell: "pgrep -fa ansible-pull || true"
          register: ansible_pull_status
          changed_when: false

        - name: Show running ansible-pull status
          debug:
            msg: "📌 [INFO] - Ansible-pull running: {{ansible_pull_status.stdout_lines }}"
          when: ansible_pull_status.stdout | length > 0

        # cronjob
        - name: Check ansible-pull cron job
          shell: "crontab -l -u {{ run_ansible_user }} | grep ansible-pull || true"
          register: cron_job_status
          changed_when: false
          failed_when: false  # Избежать ошибки, если crontab пустой
          become: true

        - name: Show cron job status
          debug:
            msg: "📌 [INFO] - Ansible-pull cron job: {{ cron_job_status.stdout_lines }}"
          when: cron_job_status.stdout | length > 0

        # Принудительное обновление фактов (чтобы загрузился новый файл)
        - name: Update ansible_facts
          ansible.builtin.setup:
            filter: ansible_local

        # Проверка наличия кастомного факта "host_role"
        - name: Show custom fact
          ansible.builtin.debug:
            msg: "Роль хоста - host_role: {{ ansible_local.custom.host_role }}"
      tags: test

Обращаю внимание на любопытный алгоритм настройки рандомного времени обновления в задачах Calculate random cron minute per host и Add ansible-pull cron job. Генерация времени обновления осуществляется на основе хеш-суммы имени хоста, что обеспечивает идемпотентность значений. Каждый раз уникальное время обновления каждого узла будет иметь одно и то же значение. Люблю такие элегантные решения.

В итоге у нас должна получится структура:

.
├── inventory.yml
└── setup_ansible_pull.yml

Игнорирование фингерпринтов SSH

  • Добавляем параметр StrictHostKeyChecking=no для автоматического подтверждения подключения.

  • Запускаем плейбук для настройки узлов.

Скрытый текст
ansible-playbook -i inventory.yml setup_ansible_pull.yml

Если тесты плейбука прошли успешно, мы увидим такую картину.

Убеждаемся в том, что созданы cron задачи
Убеждаемся в том, что созданы cron задачи
Убеждаемся в том, что хостам назначены кастомные роли
Убеждаемся в том, что хостам назначены кастомные роли

3. Конфигурирование среды для тестирования качества кода в Docker-контейнере

Выбор тестовой среды

  • В качестве среды тестирования будет использоваться контейнер Docker.

  • Контейнер будет настроен с помощью Docker Compose.

  • Тестирование будет осуществляться с подошью Ansible-lint.

Файл Docker-Compose:

Скрытый текст
# docker-compose_control.yml
version: '3.9'

services:
  ansible_control:
    build:
      context: ./docker
      dockerfile: Dockerfile.control
      args:
        DEBIAN_VERSION: ${DEBIAN_VERSION} 
        HTTP_PROXY: ${HTTP_PROXY}
        HTTPS_PROXY: ${HTTP_PROXY}
        NO_PROXY: ${NO_PROXY}
    environment:
      HTTP_PROXY: ${HTTP_PROXY}
      HTTPS_PROXY: ${HTTP_PROXY}
      NO_PROXY: ${NO_PROXY}
    container_name: ansible_control
    hostname: ansible_control
    networks:
      test_network:
        ipv4_address: 192.168.100.200
    volumes:
      - ./ansible:/ansible
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
    tty: true
    stdin_open: true
    restart: unless-stopped

networks:
  test_network:
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.100.0/24

Для описания пакетов и зависимостей, необходимых в контейнере, создадим Dockerfile:

Скрытый текст
# docker\Dockerfile.control
ARG DEBIAN_VERSION
FROM debian:12

# Задаем увеличенный таймаут и число повторных попыток для apt-get
RUN echo 'Acquire::http::Timeout "600";' >> /etc/apt/apt.conf.d/99timeout && \
    echo 'Acquire::Retries "5";' >> /etc/apt/apt.conf.d/99timeout

# Устанавливаем Ansible, ansible-lint и зависимости
RUN apt-get update && apt-get install -y \
    ansible \
    ansible-lint \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Создаем ansible-пользователя с sudo без пароля
RUN useradd -m ansible \
    && echo "ansible ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ansible \
    && chmod 0440 /etc/sudoers.d/ansible

# Проверяем установку
RUN ansible --version && ansible-lint --version

# Устанавливаем рабочую директорию
WORKDIR /ansible

CMD ["/bin/bash"]

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

4. Настройка CI/CD-пайплайна для автоматизации процесса развертывания и тестирования

Процесс работы с кодом организован следующим образом:

Процесс разработки и выпуска инфраструктурного кода

🔹 Разработка

Разработчики работают в отдельных ветках, например:

  • app – конфигурация приложений

  • web – настройка веб-серверов

  • client – клиентские компоненты

🔹 Проверка кода

При каждом коммите автоматически выполняются:

✅ Проверка синтаксиса – анализ кода на ошибки
✅ Тестирование в тестовой среде – развертывание и оценка работоспособности

🔹 Объединение изменений

Перед финальной интеграцией итоговый плейбук с ролями тестируется повторно, чтобы исключить конфликты и ошибки

🔹 Выпуск релиза

Если все тесты пройдены успешно:

✅ Плейбук отправляется в ветку release
✅ Из ветки release плейбук становится доступен клиентам через ansible-pull

Для работы нашего конвейера мы создадим gitlab-ci файл, а также токен для доступа к репозиторию, который мы определяем в переменных GitLab под именем RUNNER_TOKEN

Разберем что делает каждый job

💠 check-ansible-syntax

  • Запускает контейнер ansible_control через docker-compose.

  • Проверяет синтаксис infra_ansible_pull.yml.

  • Запускает ansible-lint, останавливает процесс при критических ошибках.

💠 test-deployment

  • Проверяет соединение с тестовыми серверами (ansible all -m ping).

  • Запускает ansible-playbook для тестового развертывания.

  • Проверяет работу Nginx и отдачу тестового шаблона через curl.

💠 deploy-ansible-role

  • Если check и test прошли успешно → пушит код в ветку release.

Скрытый текст
# .gitlab-ci.yml
variables:
  CLIENT_IP: 192.168.2.213
  CLIENT_SSH_PASSWORD: vagrant
  APP_TEMPLATE: "Это тестовый Flask-сервер"
  APP_IP: "192.168.2.212"
  WEBSERVER_IP: "192.168.2.211"

stages:
  - check
  - test
  - deploy


# 🔹 Проверяем синтаксис и Ansible-lint перед тестами
check-ansible-syntax:
  stage: check
  tags:
    - shell

  script:
    - echo "🐳 Building Ansible control node..."
    # - docker-compose -f docker-compose_control.yml down --remove-orphans
    # - docker-compose -f docker-compose_control.yml up -d --build --force-recreate
    - docker-compose -f docker-compose_control.yml up -d

    - echo "🔁 Waiting for containers to be ready..."
    - until docker ps | grep "ansible_control"; do sleep 2; done

    - echo "⏳ Waiting for 5 seconds to ensure all services are up..."
    - sleep 5

    - echo "📌 Checking Ansible playbook syntax..."
    - |
      docker exec -i ansible_control ansible-playbook infra_ansible_pull.yml --syntax-check
      if [ $? -eq 0 ]; then
        echo "✅ Синтаксис плейбука infra_ansible_pull.yml корректен!"
      else
        echo -e "\033[1;31m❌ ERROR: Ошибка в синтаксисе playbook!\033[0m"
        exit 1   # Прерываем job, если есть ошибки
      fi


    - echo "📌 Linting Ansible playbook and roles..."
    - |
      set -o pipefail # --- вывод ansible-lint ---
      # Для отладки можно включить отладочный режим:
      # set -x

      lint_log="lint_output.txt"

      # Выполнение ansible-lint, вывод логируем в файл.
      if ! docker exec -i ansible_control ansible-lint infra_ansible_pull.yml | tee "$lint_log"; then
        echo -e "\033[1;31m❌ ERROR: ansible-lint выявил критические ошибки! Устараните их согласно рекомендациям выше!\033[0m"
        exit 1
      fi

      echo ""  # Дополнительная пустая строка для корректного сканирования grep

      # Проверяем наличие предупреждений в выводе
      if grep -q "WARNING" "$lint_log"; then
        echo -e "\033[1;33m⚠️  WARNING: Обнаружены предупреждения в ansible-lint!\033[0m"
      else
        echo -e "\033[1;32m✅ Ansible-lint успешно пройден!\033[0m"
      fi
  except:
    - release  # Исключаем выполнение в ветке release

# 🔹 Развертывание Ansible playbook в тестовом окружении
test-deployment:
  stage: test
  before_script:
    - echo "♻️ Обновление окружения..."
  tags:
    - shell
  needs:
    - check-ansible-syntax  # ✅ Ждем успешного завершения синтакс-чека

  script:
    - echo "📌 Running Ansible tests - checking connection..."
    - export ANSIBLE_HOST_KEY_CHECKING=False
    - ansible all -i ansible/inventory_test.yml -m ping --extra-vars 'ansible_ssh_extra_args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"'

    - echo "📌 Running Ansible playbook execution..."
    - ansible-playbook -i ansible/inventory_test.yml ansible/infra_ansible_pull.yml -e "target_hosts=all ansible_ssh_extra_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'" # переопределяем в инвентаре целевые хосты

    # 🔹 Тесты
    # проверка доступности приложения на узле
    - echo "🧪 --- Run tests ---"
    # --- Проверка доступности Nginx
    - echo "🔍 --- Checking Nginx status"
    - |
      NGINX_STATUS=$(sshpass -p "$CLIENT_SSH_PASSWORD" ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null "vagrant@$WEBSERVER_IP" "systemctl is-active nginx" 2>/dev/null)
      if [[ "$NGINX_STATUS" == "active" ]]; then
          echo "✅ Nginx service is running"
      else
          echo "❌ Nginx service is NOT running"
          exit 1
      fi

    - echo "🔍 --- Checking app content for client"
    - |
        OUTPUT=$(sshpass -p "$CLIENT_SSH_PASSWORD" ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null "vagrant@$CLIENT_IP" "echo Узел:; hostname; echo 🌍 HTTP-ответ:; curl -s http://$WEBSERVER_IP")
        if echo "$OUTPUT" | grep -q "Узел:" && echo "$OUTPUT" | grep -q "🌍 HTTP-ответ:" && echo "$OUTPUT" | grep -q "$APP_TEMPLATE"; then
            echo "✅ Проверка успешна! HTTP-ответ содержит текст: "$APP_TEMPLATE""
            exit 0
        else
            echo "❌ Ошибка! HTTP-ответ не содержит тестовый шаблон! Ответ: "$OUTPUT""
            exit 1
        fi

    - echo "✅ All tests passed successfully!"
  except:
    - release  # Исключаем выполнение в ветке release


# 🔹 Деплой
deploy-ansible-role:
  stage: deploy
  tags:
    - shell
  needs:
    - check-ansible-syntax  # ✅ Ждем успешного завершения синтакс-чека
    - test-deployment     # ✅ Ждем успешного запуска кода на тестовых контейнерах

  script:
    - echo "🐳 Building and starting test Docker containers..."
    # create $RUNNER_TOKEN - roles - developer - read_repository, write_repository. -> Add variable to project
    - git config --global user.email "your-email@example.com"
    - git config --global user.name "andreychuyan"
    - git push --force https://oauth2:$RUNNER_TOKEN@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git  HEAD:refs/heads/release
  only:
    - main

5. Создание ролей для Ansible-pull.

Для примера развернем в нашем окружении простое тестовое приложение на Flask и настроим веб-сервер для обработки трафика.

На клиентских машинах будет выполнено:
✅ Создание сервисного пользователя
✅ Настройка доступа по RSA-ключам
✅ Вывод баннера при подключении по SSH

Структура Ansible-ролей
Каталоги и файлы организованы следующим образом:

Скрытый текст

.
├── ansible.cfg
├── group_vars
│ └── all.yml
├── infra_ansible_pull.yml
├── inventory_test.yml
├── node_setting_to_pull
│ ├── inventory.yml
│ └── setup_ansible_pull.yml
└── roles
├── app
│ ├── files
│ │ └── app.py
│ ├── handlers
│ │ └── main.yml
│ ├── README.md
│ ├── tasks
│ │ └── main.yml
│ ├── templates
│ │ └── hello-app.service.j2
│ └── vars
│ └── main.yml
├── client
│ ├── files
│ │ ├── id_rsa.pub
│ │ └── ssh_banner
│ ├── README.md
│ ├── tasks
│ │ ├── main.yml
│ │ ├── motd_setup.yml
│ │ └── user_setup.yml
│ └── templates
│ └── motd.j2
└── web
├── handlers
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
└── templates
└── nginx_application.conf.j2

Инвентарь для тестового окружения:

Скрытый текст
# ansible/inventory_test.yml
all:
  vars:
    network_prefix: "192.168.2"

  hosts:
    webserver:
      host_role: webserver
      ansible_host: "{{ network_prefix }}.211"
      ansible_user: vagrant
      ansible_password: vagrant
      ansible_become: yes
      ansible_become_method: sudo

    application:
      host_role: application
      ansible_host: "{{ network_prefix }}.212"
      ansible_user: vagrant
      ansible_password: vagrant
      ansible_become: yes
      ansible_become_method: sudo

  children:
    clients:
      hosts:
        client_1:
          host_role: client
          ansible_host: "{{ network_prefix }}.213"
          ansible_user: vagrant
          ansible_password: vagrant
          ansible_become: yes
          ansible_become_method: sudo

        client_2:
          host_role: client
          ansible_host: "{{ network_prefix }}.214"
          ansible_user: vagrant
          ansible_password: vagrant
          ansible_become: yes
          ansible_become_method: sudo

Не забудем про конфигурационный файл Ansible

Скрытый текст
# ansible/ansible.cfg
[defaults]
remote_tmp = /tmp/.ansible/tmp
roles_path = roles
remote_user = ansible
become = True
become_method = sudo
host_key_checking = False
forks = 10
timeout = 30
retry_files_enabled = False
log_path = ./ansible.log
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 600

[ssh_connection]
pipelining = True
scp_if_ssh = True

В файле групповых переменных укажем точку доступа к нашему приложению

Скрытый текст
# ansible/group_vars/all.yml
app_endpoint: "http://192.168.2.212:8080"

Вернемся к нашему корневому плейбуку и изучим его подробнее.

Обращаясь к нему, Ansible действует по алгоритму:

  1. Сбор фактов: Ansible получает информацию о хосте, включая его роль

  2. Динамическое назначение ролей:

    • Если роль хоста — webserver, применяется роль web

    • Если роль application, загружается роль app

    • Если роль client, выполняется настройка клиента

  3. Автономное применение конфигурации с Ansible-pull

Скрытый текст
# ansible/ansible/infra_ansible_pull.yaml
- name: Apply roles dynamically from inventory
  hosts: "{{ target_hosts | default('localhost') }}"
  gather_facts: true

  tasks:
    - name: Explicitly gather custom facts
      ansible.builtin.setup:
        filter: ansible_local

    - name: Include web role
      ansible.builtin.include_role:
        name: web
      when: ansible_local.custom.host_role is defined and ansible_local.custom.host_role == "webserver"

    - name: Include app role
      ansible.builtin.include_role:
        name: app
      when: ansible_local.custom.host_role is defined and ansible_local.custom.host_role == "application"

    - name: Include client role
      ansible.builtin.include_role:
        name: client
      when: ansible_local.custom.host_role is defined and ansible_local.custom.host_role == "client"

Кратко рассмотрим как наши роли работают

Роль web разворачивает nginx для доступа внешних клиентов к приложению:

Устанавливает Nginx (Ubuntu/Debian)
✅ Запускает и включает сервис
✅ Размещает конфигурационный файл из шаблона
✅ Включает сайт, удаляет дефолтный конфиг
✅ Проверяет конфигурацию и перезапускает Nginx

Скрытый текст
# ansible/roles/web/handlers/main.yml
- name: Reload Nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded
  become: true
# ansible/roles/web/tasks/main.yml
# установка nginx
- name: Ensure Nginx is installed (Ubuntu/Debian)
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
  become: true
  when: ansible_os_family == "Debian"

- name: Ensure Nginx is enabled and running
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true
  become: true
  when: ansible_os_family == "Debian"

- name: Create Nginx configuration file for application
  ansible.builtin.template:
    src: nginx_application.conf.j2  # Путь к вашему шаблону Jinja2
    dest: /etc/nginx/sites-available/application
    mode: '0644'
  become: true

- name: Enable Nginx site configuration
  ansible.builtin.file:
    src: /etc/nginx/sites-available/application
    dest: /etc/nginx/sites-enabled/application
    state: link
  become: true
  notify: Reload Nginx

# ❌ Удаляем дефолтный конфиг Nginx
- name: Remove default Nginx site configuration
  ansible.builtin.file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  become: true

- name: Test Nginx configuration
  ansible.builtin.command: nginx -t
  become: true
  changed_when: false  # <- Сообщает Ansible, что это не изменяющее действие

- name: Restart Nginx to apply changes
  ansible.builtin.service:
    name: nginx
    state: restarted
    enabled: true
  become: true
# ansible/roles/web/templates/nginx_application.conf.j2
server {
    listen 80;
    server_name _;

    location / {
        # Укажите IP-адрес или доменное имя вашего приложения и порт:
        proxy_pass {{ app_endpoint }};
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Роль app разворачивает простое веб приложение для демонстрации:

✅ Установка Python и pip
✅ Создание виртуального окружения
✅ Установка Flask и requests
✅ Развёртывание Flask-приложения
✅ Настройка systemd-сервиса для его автоматического запуска

Скрытый текст
# ansible/roles/app/tasks/main.yml
# *Установка Python и Pip на Debian/Ubuntu*
- name: Install Python and Pip (Debian)
  become: true
  ansible.builtin.apt:
    name:
      - python3-apt
      - python3
      - python3-pip
      - python3-venv
    state: present
    update_cache: true
  when: ansible_os_family == "Debian"

# Создание виртуального окружения
- name: Create Python virtual environment
  ansible.builtin.command: "python3 -m venv {{ venv_path }}"
  args:
    creates: "{{ venv_path }}"

# Обновление pip внутри виртуального окружения
- name: Upgrade pip inside virtual environment
  ansible.builtin.pip:
    name: pip
    state: present
    extra_args: --upgrade
    virtualenv: "{{ venv_path }}"

# Установка дополнительных пакетов (Flask и requests) в виртуальное окружение
- name: Install required Python packages in virtual environment
  ansible.builtin.pip:
    name:
      - requests
      - flask
    virtualenv: "{{ venv_path }}"

# *Проверка установки Python*
- name: Verify Python installation
  ansible.builtin.command: python3 --version
  register: python_version
  changed_when: false

- name: Debug Python version
  ansible.builtin.debug:
    msg: "Installed Python version: {{ python_version.stdout }}"

# *Проверка установки Pip (системного)*
- name: Verify Pip installation
  ansible.builtin.command: pip3 --version
  register: pip_version
  changed_when: false

- name: Debug Pip version
  ansible.builtin.debug:
    msg: "Installed Pip version: {{ pip_version.stdout }}"

# *Проверка, что виртуальное окружение успешно создано*
- name: Verify virtual environment
  ansible.builtin.stat:
    path: "{{ venv_path }}/bin/activate"
  register: venv_check

- name: Debug virtual environment status
  ansible.builtin.debug:
    msg: "Virtual environment exists at {{ venv_path }}"
  when: venv_check.stat.exists

# ====== Развёртывание Flask-приложения ======

# Создание директории для приложения
- name: Create application directory
  ansible.builtin.file:
    path: "{{ app_dir }}"
    state: directory
    mode: '0755'

# Развёртывание файла приложения app.py
- name: Deploy Flask application (app.py)
  ansible.builtin.copy:
    src: app.py
    dest: "{{ app_dir }}/app.py"
    mode: '0755'

# Создание systemd-сервиса для автоматического запуска приложения
- name: Create systemd service for Flask app
  become: true
  ansible.builtin.template:
    src: hello-app.service.j2
    dest: /etc/systemd/system/hello-app.service
    mode: '0644'
  notify: Reload systemd

# Включение и запуск Flask приложения через systemd
- name: Ensure hello-app service is enabled and started
  become: true
  ansible.builtin.systemd:
    name: hello-app
    state: restarted
    enabled: true
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ansible/roles/app/files

from flask import Flask, render_template_string
from datetime import datetime
import random

app = Flask(__name__)

QUOTES = [
    "Жизнь — это то, что с тобой происходит, пока ты строишь планы. — Джон Леннон",
    "Секрет успеха в том, чтобы начать. — Марк Твен",
    "Сложные дороги ведут к красивым направлениям. — Неизвестный автор",
    "Начни делать — и энергия появится. — Джеймс Клир",
    "Будущее принадлежит тем, кто верит в красоту своей мечты. — Элеонор Рузвельт"
]

def get_current_time():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def get_random_quote():
    return random.choice(QUOTES)

HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Тестовое приложение</title>
    <link 
        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" 
        rel="stylesheet">
    <style>
        body { background-color: #f8f9fa; text-align: center; padding: 5rem; }
        .container { background: white; padding: 2rem; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
        h1 { color: #007bff; }
        p { font-size: 1.2rem; }
        .quote { font-style: italic; color: #6c757d; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 Добро пожаловать!</h1>
        <p>Это тестовый Flask-сервер.</p>
        <p><strong>Текущее время:</strong> {{ time }}</p>
        <p class="quote">📜 Цитата дня: "{{ quote }}"</p>
        <a href="/time" class="btn btn-primary mt-3">Показать только время</a>
    </div>
</body>
</html>
"""

@app.route('/')
def home():
    return render_template_string(HTML_TEMPLATE, time=get_current_time(), quote=get_random_quote())

@app.route('/time')
def show_time():
    return {"current_time": get_current_time()}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, debug=True)
# ansible/roles/app/handlers/main.yml
- name: Reload systemd
  become: true
  ansible.builtin.systemd:
    daemon_reload: true
# ansible/roles/app/templates/hello-app.service.j2
[Unit]
Description=Hello Flask App Service
After=network.target

[Service]
User={{ ansible_user_id }}
WorkingDirectory={{ app_dir }}
ExecStart={{ venv_path }}/bin/python app.py
Restart=always

[Install]
WantedBy=multi-user.target
# ansible/roles/app/vars/main.yml
venv_path: "{{ ansible_env.HOME }}/.venv"
app_dir: "{{ ansible_env.HOME }}/hello_app"

Роль client настраивает сервисного пользователя, rsa-авторизацию и ssh баннер. Не забываем перенести публичный ключ в ansible/roles/client/files/id_rsa.pub

Скрытый текст
# ansible/roles/client/tasks/main.yml
---
- name: Создание пользователя и настройка SSH
  ansible.builtin.import_tasks: user_setup.yml

- name: Настройка MOTD / SSH баннера
  ansible.builtin.import_tasks: motd_setup.yml
# ansible/roles/client/tasks/motd_setup.yml
---
- name: Setup MOTD (message day)
  ansible.builtin.template:
    src: templates/motd.j2
    dest: /etc/motd
    owner: root
    group: root
    mode: '0644'  # Установим безопасные права
  become: true

- name: Install SSH banner
  ansible.builtin.copy:
    src: files/ssh_banner
    dest: /etc/issue.net
    owner: root
    group: root
    mode: '0644'
  become: true

- name: Enable SSH banner
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?Banner'
    line: 'Banner /etc/issue.net'
    state: present
  notify: Restart SSH
  become: true
# ansible/roles/client/tasks/user_setup.yml
---
- name: Create user ladmin
  ansible.builtin.user:
    name: ladmin
    shell: /bin/bash
    groups: "{{ 'sudo' if ansible_os_family == 'Debian' else 'wheel' }}"
    append: true
    create_home: true
    state: present
  become: true

- name: Copy SSH-keys for user ladmin
  ansible.posix.authorized_key:
    user: ladmin
    state: present
    key: "{{ lookup('file', 'files/id_rsa.pub') }}"

- name: Create sudo no pass user ladmin
  ansible.builtin.copy:
    dest: /etc/sudoers.d/ladmin
    content: "ladmin ALL=(ALL) NOPASSWD:ALL"
    mode: "0440"
  become: true

ansible/roles/client/files/ssh_banner

🔒 Внимание! Это частный сервер. Неавторизованный доступ запрещён! 🔒
# ansible/roles/client/handlers/main.yml
- name: Restart SSH
  ansible.builtin.service:
    name: "{{ 'sshd' if ansible_os_family == 'RedHat' else 'ssh' }}"
    state: restarted

- name: Reload environment
  ansible.builtin.command: source /etc/environment
  changed_when: false

ansible/roles/client/templates/motd.j2

Добро пожаловать на сервер {{ ansible_hostname }}!
Дата и время: {{ ansible_date_time.date }}, {{ ansible_date_time.time }}
ОС: {{ ansible_distribution }} {{ ansible_distribution_version }}
CPU: {{ ansible_processor_cores }} ядер
RAM: {{ ansible_memtotal_mb }} MB

Проверка работы ansible-pull

Делаем коммит и пуш, и наслаждаемся успешными тестами CI/CD!
Делаем коммит и пуш, и наслаждаемся успешными тестами CI/CD!

Спустя 30 минут, все наши клиенты должны с помощью ansible-pull автоматически применить необходимую конфигурацию из репозитория.

Перейдя на веб-сервер в продакшене, мы должны увидеть сгенерированную нашим приложением веб-страницу.

Чтобы проверить как клиенты применяют плейбуки мы можем проверить лог одной из клиентских машин /var/log/ansible-pull.log

Скрытый текст

/var/log/ansible-pull.log

ansible@p-application:~$ cat /var/log/ansible-pull.log
[WARNING]: Could not match supplied host pattern, ignoring: p-application
localhost | SUCCESS => {
"after": "ba7fba1e999fe5e3a4f49d09834601e4c2481e6e",
"before": "ba7fba1e999fe5e3a4f49d09834601e4c2481e6e",
"changed": false,
"remote_url_changed": false
}
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'
[WARNING]: Could not match supplied host pattern, ignoring: p-application

PLAY [Apply roles dynamically from inventory] **********************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Explicitly gather custom facts] ******************************************
ok: [localhost]

TASK [Include web role] ********************************************************
skipping: [localhost]

TASK [Include app role] ********************************************************

TASK [app : Install Python and Pip (Debian)] ***********************************
ok: [localhost]

TASK [app : Create Python virtual environment] *********************************
ok: [localhost]

TASK [app : Upgrade pip inside virtual environment] ****************************
ok: [localhost]

TASK [app : Install required Python packages in virtual environment] ***********
ok: [localhost]

TASK [app : Verify Python installation] ****************************************
ok: [localhost]

TASK [app : Debug Python version] **********************************************
ok: [localhost] => {
"msg": "Installed Python version: Python 3.11.2"
}

TASK [app : Verify Pip installation] *******************************************
ok: [localhost]

TASK [app : Debug Pip version] *************************************************
ok: [localhost] => {
"msg": "Installed Pip version: pip 23.0.1 from /usr/lib/python3/dist-packages/pip (python 3.11)"
}

TASK [app : Verify virtual environment] ****************************************
ok: [localhost]

TASK [app : Debug virtual environment status] **********************************
ok: [localhost] => {
"msg": "Virtual environment exists at /home/ansible/.venv"
}

TASK [app : Create application directory] **************************************
ok: [localhost]

TASK [app : Deploy Flask application (app.py)] *********************************
ok: [localhost]

TASK [app : Create systemd service for Flask app] ******************************
ok: [localhost]

TASK [app : Ensure hello-app service is enabled and started] *******************
changed: [localhost]

TASK [Include client role] *****************************************************
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost : ok=16 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0

Starting Ansible Pull at 2025-03-14 23:29:53
/usr/bin/ansible-pull -U git@gitlab.ch.ap:AndreyChuyan/ansible_pull_cicd.git -C release --private-key /home/ansible/.ssh/id_ansible_pull ansible/infra_ansible_pull.yml

Из вывода можно наблюдать, что все задачи успешно применены.

Мониторинг ошибок и обновлений
В дальнейшем можно отслеживать ошибки и статус обновлений с помощью пользовательских экспортёров Prometheus или ELK, но это уже тема для отдельной статьи.

Спасибо, что дочитали статью до конца! Буду рад вашей обратной связи.

Only registered users can participate in poll. Log in, please.
Какой инструментарий вы используете для pull-а конфигураций?
35.29% Ansible-pull6
17.65% SaltStack3
17.65% Puppet3
5.88% Chef1
11.76% Custom Bash/Python-скрипты2
29.41% Другие инструменты? Напишите в комментариях!5
17 users voted. 11 users abstained.
Tags:
Hubs:
Total votes 14: ↑13 and ↓1+13
Comments18

Articles