--- - name: Deploy Docker Image to Application Stack hosts: "{{ deployment_hosts | default('production') }}" gather_facts: yes become: no vars: # Application code directory (where docker-compose files are located) application_code_dest: "/home/deploy/michaelschiemer/current" application_compose_suffix: >- {%- if deployment_environment == 'staging' -%} staging.yml {%- else -%} production.yml {%- endif -%} # Image to deploy (can be overridden via -e image_tag=...) image_tag: "{{ image_tag | default('latest') }}" docker_registry: "{{ docker_registry | default('registry.michaelschiemer.de') }}" app_name_default: "framework" # Deployment environment (staging or production) deployment_environment: "{{ deployment_environment | default('production') }}" tasks: - name: Check if vault file exists locally stat: path: "{{ playbook_dir }}/../secrets/{{ deployment_environment }}.vault.yml" delegate_to: localhost register: vault_file_stat become: no - name: Load secrets from vault file if exists include_vars: file: "{{ playbook_dir }}/../secrets/{{ deployment_environment }}.vault.yml" when: vault_file_stat.stat.exists no_log: yes ignore_errors: yes delegate_to: localhost become: no - name: Set app_name from provided value or default ansible.builtin.set_fact: app_name: "{{ app_name if (app_name is defined and app_name != '') else app_name_default }}" - name: Extract registry URL from docker-compose file (for image deployment) ansible.builtin.shell: | cd {{ application_code_dest }} grep -h "image:" docker-compose.base.yml docker-compose.{{ application_compose_suffix }} 2>/dev/null | \ grep -E "{{ app_name }}" | head -1 | \ sed -E 's/.*image:\s*([^\/]+).*/\1/' | \ sed -E 's/\/.*$//' || echo "localhost:5000" register: compose_registry_url changed_when: false failed_when: false - name: Set local registry (where containers expect the image) ansible.builtin.set_fact: local_registry: "{{ (compose_registry_url.stdout | trim) if (compose_registry_url.stdout | trim != '') else 'localhost:5000' }}" - name: Set source registry (where workflow pushes the image) ansible.builtin.set_fact: source_registry: "{{ docker_registry }}" - name: Set deploy_image from source registry (for pulling) ansible.builtin.set_fact: deploy_image: "{{ source_registry }}/{{ app_name }}:{{ image_tag }}" - name: Set local_image (where containers expect the image) ansible.builtin.set_fact: local_image: "{{ local_registry }}/{{ app_name }}:{{ image_tag }}" - name: Set database and MinIO variables from vault or defaults ansible.builtin.set_fact: db_username: "{{ db_username | default(vault_db_user | default('postgres')) }}" db_password: "{{ db_password | default(vault_db_password | default('')) }}" minio_root_user: "{{ minio_root_user | default(vault_minio_root_user | default('minioadmin')) }}" minio_root_password: "{{ minio_root_password | default(vault_minio_root_password | default('')) }}" secrets_dir: "{{ secrets_dir | default('./secrets') }}" git_repository_url: "{{ git_repository_url | default(vault_git_repository_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git')) }}" git_branch: >- {%- if deployment_environment == 'staging' -%} staging {%- else -%} main {%- endif -%} git_token: "{{ git_token | default(vault_git_token | default('')) }}" git_username: "{{ git_username | default(vault_git_username | default('')) }}" git_password: "{{ git_password | default(vault_git_password | default('')) }}" no_log: yes - name: Determine Docker registry password from vault or extra vars ansible.builtin.set_fact: registry_password: >- {%- if docker_registry_password is defined and docker_registry_password | string | trim != '' -%} {{ docker_registry_password }} {%- elif vault_docker_registry_password is defined and vault_docker_registry_password | string | trim != '' -%} {{ vault_docker_registry_password }} {%- else -%} {{ '' }} {%- endif -%} no_log: yes - name: Check if registry is accessible ansible.builtin.uri: url: "https://{{ docker_registry }}/v2/" method: GET status_code: [200, 401] timeout: 5 validate_certs: no register: registry_check ignore_errors: yes delegate_to: "{{ inventory_hostname }}" become: no - name: Set registry accessible flag ansible.builtin.set_fact: registry_accessible: "{{ 'true' if (registry_check.status is defined and registry_check.status | int in [200, 401]) else 'false' }}" - name: Login to Docker registry community.docker.docker_login: registry_url: "{{ docker_registry }}" username: "{{ docker_registry_username | default('admin') }}" password: "{{ registry_password }}" when: - registry_password | string | trim != '' - registry_accessible == 'true' no_log: yes ignore_errors: yes register: docker_login_result - name: Display image pull information ansible.builtin.debug: msg: - "Attempting to pull image: {{ deploy_image }}" - "Source registry: {{ source_registry }}" - "Local registry: {{ local_registry }}" - "Registry accessible: {{ registry_accessible | default('unknown') }}" when: registry_accessible is defined and registry_accessible == 'true' - name: Check if image already exists locally ansible.builtin.shell: | docker images --format "{{ '{{' }}.Repository{{ '}}' }}:{{ '{{' }}.Tag{{ '}}' }}" | grep -E "^{{ deploy_image | regex_escape }}$" || echo "NOT_FOUND" register: image_exists_before_pull when: registry_accessible is defined and registry_accessible == 'true' changed_when: false failed_when: false - name: Pull Docker image (force pull if not exists locally) community.docker.docker_image: name: "{{ deploy_image }}" source: pull pull: always when: - registry_accessible is defined and registry_accessible == 'true' - image_exists_before_pull.stdout | default('') == 'NOT_FOUND' register: image_pull_result ignore_errors: yes failed_when: false - name: Pull Docker image (if already exists, just verify) community.docker.docker_image: name: "{{ deploy_image }}" source: pull pull: true when: - registry_accessible is defined and registry_accessible == 'true' - image_exists_before_pull.stdout | default('') != 'NOT_FOUND' register: image_pull_result ignore_errors: yes failed_when: false - name: Display pull result ansible.builtin.debug: msg: - "Pull result: {{ image_pull_result | default('not executed') }}" - "Pull failed: {{ image_pull_result.failed | default(false) }}" - "Pull changed: {{ image_pull_result.changed | default(false) }}" when: registry_accessible is defined and registry_accessible == 'true' - name: Verify image exists locally after pull community.docker.docker_image_info: name: "{{ deploy_image }}" register: image_info when: registry_accessible is defined and registry_accessible == 'true' ignore_errors: yes failed_when: false - name: Check if image exists by inspecting docker images ansible.builtin.shell: | docker images --format "{{ '{{' }}.Repository{{ '}}' }}:{{ '{{' }}.Tag{{ '}}' }}" | grep -E "^{{ deploy_image | regex_escape }}$" || echo "NOT_FOUND" register: image_check when: registry_accessible is defined and registry_accessible == 'true' changed_when: false failed_when: false - name: Display image verification results ansible.builtin.debug: msg: - "Image info result: {{ image_info | default('not executed') }}" - "Image check result: {{ image_check.stdout | default('not executed') }}" - "Image exists: {{ 'YES' if (image_check.stdout | default('') != 'NOT_FOUND' and image_check.stdout | default('') != '') else 'NO' }}" when: registry_accessible is defined and registry_accessible == 'true' - name: Fail if image was not pulled successfully ansible.builtin.fail: msg: | Failed to pull image {{ deploy_image }} from registry. The image does not exist locally after pull attempt. Pull result: changed={{ image_pull_result.changed | default(false) }}, failed={{ image_pull_result.failed | default(false) }} Please check: 1. Does the image exist in {{ source_registry }}? 2. Are registry credentials correct? 3. Is the registry accessible? when: - registry_accessible is defined and registry_accessible == 'true' - (image_check.stdout | default('') == 'NOT_FOUND' or image_check.stdout | default('') == '') - name: Tag image for local registry (if source and local registry differ) community.docker.docker_image: name: "{{ deploy_image }}" repository: "{{ local_image }}" tag: "{{ image_tag }}" source: local when: - source_registry != local_registry - image_check.stdout is defined - image_check.stdout != 'NOT_FOUND' - image_check.stdout != '' register: image_tag_result - name: Push image to local registry (if source and local registry differ) community.docker.docker_image: name: "{{ local_image }}" push: true source: local when: - source_registry != local_registry - image_tag_result.changed | default(false) ignore_errors: yes failed_when: false - name: Update docker-compose file with new image tag ansible.builtin.replace: path: "{{ application_code_dest }}/docker-compose.{{ application_compose_suffix }}" regexp: '^(\s+image:\s+)({{ local_registry }}/{{ app_name }}:)(.*)$' replace: '\1\2{{ image_tag }}' register: compose_update_result failed_when: false changed_when: compose_update_result.changed | default(false) - name: Update docker-compose file with new image (alternative pattern - any registry) ansible.builtin.replace: path: "{{ application_code_dest }}/docker-compose.{{ application_compose_suffix }}" regexp: '^(\s+image:\s+)([^\/]+\/{{ app_name }}:)(.*)$' replace: '\1{{ local_registry }}/{{ app_name }}:{{ image_tag }}' register: compose_update_alt when: compose_update_result.changed == false failed_when: false changed_when: compose_update_alt.changed | default(false) - name: Ensure Docker networks exist community.docker.docker_network: name: "{{ item }}" state: present loop: - traefik-public - app-internal ignore_errors: yes - name: Check if .env file exists stat: path: "{{ application_code_dest }}/.env" register: env_file_exists - name: Create minimal .env file if it doesn't exist copy: dest: "{{ application_code_dest }}/.env" content: | # Minimal .env file for Docker Compose # This file should be properly configured by the application setup playbook DB_USERNAME={{ db_username | default('postgres') }} DB_PASSWORD={{ db_password | default('') }} MINIO_ROOT_USER={{ minio_root_user | default('minioadmin') }} MINIO_ROOT_PASSWORD={{ minio_root_password | default('') }} SECRETS_DIR={{ secrets_dir | default('./secrets') }} GIT_REPOSITORY_URL={{ git_repository_url | default('') }} GIT_TOKEN={{ git_token | default('') }} GIT_USERNAME={{ git_username | default('') }} GIT_PASSWORD={{ git_password | default('') }} owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0600' when: not env_file_exists.stat.exists become: yes - name: Check if Docker daemon.json exists stat: path: /etc/docker/daemon.json register: docker_daemon_json become: yes - name: Read existing Docker daemon.json slurp: src: /etc/docker/daemon.json register: docker_daemon_config when: docker_daemon_json.stat.exists become: yes changed_when: false - name: Set Docker daemon configuration with insecure registry set_fact: docker_daemon_config_dict: "{{ docker_daemon_config.content | b64decode | from_json if (docker_daemon_json.stat.exists and docker_daemon_config.content is defined) else {} }}" - name: Build insecure registries list set_fact: insecure_registries_list: >- {%- set existing = docker_daemon_config_dict.get('insecure-registries', []) | list -%} {%- set needed_registries = [local_registry] | list -%} {%- set all_registries = (existing + needed_registries) | unique | list -%} {{ all_registries }} - name: Merge insecure registry into Docker daemon config set_fact: docker_daemon_config_merged: "{{ docker_daemon_config_dict | combine({'insecure-registries': insecure_registries_list}) }}" - name: Update Docker daemon.json with insecure registry copy: dest: /etc/docker/daemon.json content: "{{ docker_daemon_config_merged | to_json(indent=2) }}" mode: '0644' when: docker_daemon_config_merged != docker_daemon_config_dict become: yes register: docker_daemon_updated - name: Restart Docker daemon if configuration changed systemd: name: docker state: restarted when: docker_daemon_updated.changed | default(false) become: yes ignore_errors: yes - name: Wait for Docker daemon to be ready shell: docker ps > /dev/null 2>&1 register: docker_ready until: docker_ready.rc == 0 retries: 30 delay: 2 when: docker_daemon_updated.changed | default(false) ignore_errors: yes changed_when: false - name: Set list of registries to login to (source registry for pulling, local registry for pushing) ansible.builtin.set_fact: registries_to_login: >- {%- if source_registry is defined and local_registry is defined -%} {%- if source_registry != local_registry -%} {%- set reg_list = [source_registry, local_registry] -%} {%- else -%} {%- set reg_list = [local_registry] -%} {%- endif -%} {%- set final_list = reg_list | unique | list -%} {{ final_list }} {%- else -%} {%- set default_list = [docker_registry | default('localhost:5000')] -%} {{ default_list }} {%- endif -%} - name: Login to all Docker registries before compose up community.docker.docker_login: registry_url: "{{ item }}" username: "{{ docker_registry_username | default('admin') }}" password: "{{ registry_password }}" when: - registry_password | string | trim != '' - registry_accessible == 'true' loop: "{{ registries_to_login | default([docker_registry]) }}" no_log: yes register: docker_login_results failed_when: false - name: Display login results ansible.builtin.debug: msg: "Docker login to {{ item.item }}: {% if item.failed %}FAILED ({{ item.msg | default('unknown error') }}){% else %}SUCCESS{% endif %}" when: - registry_password | string | trim != '' - registry_accessible == 'true' loop: "{{ docker_login_results.results | default([]) }}" loop_control: label: "{{ item.item }}" - name: Deploy application stack with new image shell: | cd {{ application_code_dest }} docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} up -d --pull missing --force-recreate --remove-orphans register: compose_deploy_result changed_when: true environment: DB_USERNAME: "{{ db_username | default('postgres') }}" DB_PASSWORD: "{{ db_password | default('') }}" MINIO_ROOT_USER: "{{ minio_root_user | default('minioadmin') }}" MINIO_ROOT_PASSWORD: "{{ minio_root_password | default('') }}" SECRETS_DIR: "{{ secrets_dir | default('./secrets') }}" GIT_REPOSITORY_URL: "{{ git_repository_url | default('') }}" GIT_BRANCH: "{{ git_branch | default('main') }}" GIT_TOKEN: "{{ git_token | default('') }}" GIT_USERNAME: "{{ git_username | default('') }}" GIT_PASSWORD: "{{ git_password | default('') }}" - name: Wait for containers to start ansible.builtin.pause: seconds: 15 - name: Check container status shell: | cd {{ application_code_dest }} docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} ps register: container_status changed_when: false - name: Display deployment summary ansible.builtin.debug: msg: | ======================================== Image Deployment Summary ======================================== Image: {{ deploy_image }} Tag: {{ image_tag }} Environment: {{ deployment_environment }} Stack: {{ application_code_dest }} Status: SUCCESS ======================================== Container Status: {{ container_status.stdout | default('Not available') }} ========================================