--- - 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: Set deploy_image from registry, app_name and tag ansible.builtin.set_fact: deploy_image: "{{ docker_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: Pull Docker image community.docker.docker_image: name: "{{ deploy_image }}" source: pull pull: true when: registry_accessible is defined and registry_accessible == 'true' register: image_pull_result ignore_errors: yes failed_when: false - name: Verify image exists locally community.docker.docker_image_info: name: "{{ deploy_image }}" register: image_info failed_when: image_info.failed | default(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+)({{ docker_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) ansible.builtin.replace: path: "{{ application_code_dest }}/docker-compose.{{ application_compose_suffix }}" regexp: 'image:\s+{{ docker_registry }}/{{ app_name }}:.*' replace: 'image: {{ deploy_image }}' 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 -%} {%- if 'git.michaelschiemer.de:5000' not in existing -%} {{ existing + ['git.michaelschiemer.de:5000'] }} {%- else -%} {{ existing }} {%- endif -%} - 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: Extract registry URLs with ports from docker-compose files ansible.builtin.shell: | cd {{ application_code_dest }} grep -h "image:" docker-compose.base.yml docker-compose.{{ application_compose_suffix }} 2>/dev/null | \ sed -E 's/.*image:\s*([^\/]+).*/\1/' | \ sed -E 's/\/.*$//' | \ sort -u || echo "" register: actual_registry_urls_full changed_when: false failed_when: false - name: Set list of registries to login to (filter out service names, preserve ports) ansible.builtin.set_fact: registries_to_login: >- {%- set found_registries = actual_registry_urls_full.stdout | trim | split('\n') | select('match', '.+') | list -%} {%- set filtered_registries = [] -%} {%- for reg in found_registries -%} {%- if reg | regex_search('\.(de|com|org|net|io|dev)') or reg | regex_search(':[0-9]+') or reg == 'localhost' -%} {%- set _ = filtered_registries.append(reg) -%} {%- endif -%} {%- endfor -%} {%- set default_registry = [docker_registry] -%} {%- if filtered_registries | length > 0 -%} {{ filtered_registries | unique | list }} {%- else -%} {{ default_registry }} {%- 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') }} ========================================