--- # tasks file for app_image # # Роль-инициатор сборки. Гоняет команду сборки приложения на control-хосте и # доставляет получившийся образ на сервер через docker save/load. Тулчейн сборки # (Go, node, …) на сервере не нужен — туда едет готовый образ. # # Контракт приложения (app_image_command): команда в app_image_src_dir получает # BUILD_ID из окружения и собирает полный образ :$BUILD_ID. # # Тег образа = случайный BUILD_ID (роль его генерит). Дедупликации по содержимому # нет — каждый деплой везёт образ и пересоздаёт контейнер. Для ручного нечастого # деплоя это осознанный выбор: простота важнее экономии рестарта. Старые образы # на сервере чистит `docker image prune -af` по крону (см. playbook-system.yml). # # Рассчитана на один целевой хост за прогон: сборка делегируется на control-хост, # при нескольких хостах в play повторится на каждый (мы деплоим последовательно). # # Выход: факт app_image_tag (= BUILD_ID) + образ : # на сервере. Тег забирает следующий кирпичик (рендер compose). - name: "Validate role input" ansible.builtin.assert: that: # Формат имени docker-образа: он же идёт в имя tar-файла (vars/main.yml), # поэтому '/' и прочее ломало бы путь и docker-референс. - app_image_name is string and app_image_name is match('^[a-z0-9][a-z0-9._-]*$') - app_image_src_dir is string and app_image_src_dir | length > 0 - app_image_command is string and app_image_command | length > 0 fail_msg: >- app_image_name — валидное имя docker-образа (^[a-z0-9][a-z0-9._-]*$); app_image_src_dir и app_image_command — непустые строки quiet: true - name: "Generate build id (image tag)" ansible.builtin.set_fact: app_image_tag: "{{ lookup('ansible.builtin.password', '/dev/null chars=ascii_lowercase,digits length=16') }}" - name: "Build image and deliver it to server" # В --check command-таски пропускаются, tar не создаётся и copy упал бы «нет # файла на контроллере». Пропускаем всю доставку — факт app_image_tag уже # выставлен, поэтому рендер compose ниже честно покажет diff со своим тегом. when: "not ansible_check_mode" block: - name: "Build image locally via app contract" delegate_to: localhost # Сборка идёт от локального юзера. Инвентарь включает become глобально, но # локальная сборка под root не нужна — фиксируем инвариант явно. become: false ansible.builtin.command: cmd: "{{ app_image_command }}" chdir: "{{ app_image_src_dir }}" environment: BUILD_ID: "{{ app_image_tag }}" # Локальная сборка — средство доставки; факт изменения системы (сервера) # отражает загрузка образа ниже. changed_when: false - name: "Save image to tar locally" delegate_to: localhost become: false ansible.builtin.command: cmd: "docker save --output {{ app_image_local_tar }} {{ app_image_name }}:{{ app_image_tag }}" changed_when: false - name: "Copy image tar to server" ansible.builtin.copy: src: "{{ app_image_local_tar }}" dest: "{{ app_image_remote_tar }}" mode: "0644" - name: "Load image on server" ansible.builtin.command: cmd: "docker load --input {{ app_image_remote_tar }}" changed_when: true always: - name: "Remove local image tar" delegate_to: localhost become: false ansible.builtin.file: path: "{{ app_image_local_tar }}" state: absent - name: "Remove remote image tar" ansible.builtin.file: path: "{{ app_image_remote_tar }}" state: absent # Локальный тег — временная ручка (на сервер образ уехал через save/load). # Снимаем, чтобы control-хост не копил : от каждого прогона. - name: "Remove transient local image tag" delegate_to: localhost become: false ansible.builtin.command: cmd: "docker image rm {{ app_image_name }}:{{ app_image_tag }}" changed_when: false failed_when: false