Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение26 мин
Количество просмотров6.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, но это уже тема для отдельной статьи.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какой инструментарий вы используете для pull-а конфигураций?
35.29% Ansible-pull6
17.65% SaltStack3
17.65% Puppet3
5.88% Chef1
11.76% Custom Bash/Python-скрипты2
29.41% Другие инструменты? Напишите в комментариях!5
Проголосовали 17 пользователей. Воздержались 10 пользователей.
Теги:
Хабы:
Всего голосов 14: ↑13 и ↓1+13
Комментарии18

Публикации

Работа

DevOps инженер
30 вакансий

Ближайшие события