--- - name: Deploy Application Update via Docker Compose hosts: production gather_facts: yes become: no vars: # These should be passed via -e from CI/CD image_tag: "{{ image_tag | default('latest') }}" git_commit_sha: "{{ git_commit_sha | default('unknown') }}" deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}" # app_stack_path is now defined in group_vars/production.yml pre_tasks: - name: Optionally load registry credentials from encrypted vault include_vars: file: "{{ playbook_dir }}/../secrets/production.vault.yml" no_log: yes ignore_errors: yes delegate_to: localhost become: no - name: Derive docker registry credentials from vault when not provided set_fact: docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(docker_registry_username_default)) }}" docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(docker_registry_password_default)) }}" - name: Ensure system packages are up to date include_role: name: system when: system_update_packages | bool - name: Verify Docker is running systemd: name: docker state: started register: docker_service become: yes - name: Fail if Docker is not running fail: msg: "Docker service is not running" when: docker_service.status.ActiveState != 'active' - name: Ensure application stack directory exists file: path: "{{ app_stack_path }}" state: directory owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0755' - name: Check if docker-compose.yml exists in application stack stat: path: "{{ app_stack_path }}/docker-compose.yml" register: compose_file_exists - name: Fail if docker-compose.yml doesn't exist fail: msg: | Application Stack docker-compose.yml not found at {{ app_stack_path }}/docker-compose.yml The Application Stack must be deployed first via: ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml This will create the application stack with docker-compose.yml and .env file. when: not compose_file_exists.stat.exists - name: Create backup directory file: path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}" state: directory owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0755' tasks: - name: Verify docker-compose.yml exists stat: path: "{{ app_stack_path }}/docker-compose.yml" register: compose_file_check - name: Fail if docker-compose.yml doesn't exist fail: msg: | Application Stack docker-compose.yml not found at {{ app_stack_path }}/docker-compose.yml The Application Stack must be deployed first via: ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml This will create the application stack with docker-compose.yml and .env file. when: not compose_file_check.stat.exists - name: Backup current deployment metadata shell: | docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true docker compose -f {{ app_stack_path }}/docker-compose.yml config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true args: executable: /bin/bash changed_when: false ignore_errors: yes when: compose_file_check.stat.exists - name: Login to Docker registry (if credentials provided) community.docker.docker_login: registry_url: "{{ docker_registry_url }}" username: "{{ docker_registry_username }}" password: "{{ docker_registry_password }}" no_log: yes ignore_errors: yes when: - docker_registry_username is defined - docker_registry_password is defined - docker_registry_username | length > 0 - docker_registry_password | length > 0 register: registry_login - name: Pull new Docker image community.docker.docker_image: name: "{{ app_image }}" tag: "{{ image_tag }}" source: pull force_source: yes register: image_pull - name: Verify image was pulled successfully fail: msg: "Failed to pull image {{ app_image }}:{{ image_tag }}" when: image_pull.failed - name: Update docker-compose.yml with new image tag (all services) replace: path: "{{ app_stack_path }}/docker-compose.yml" # Match both localhost:5000 and registry.michaelschiemer.de (or any registry URL) regexp: '^(\s+image:\s+)(localhost:5000|registry\.michaelschiemer\.de|{{ docker_registry }})/{{ app_name }}:.*$' replace: '\1{{ app_image }}:{{ image_tag }}' # Always update to ensure localhost:5000 is used (registry only accessible via localhost) when: true register: compose_updated - name: Redeploy application stack with new image import_role: name: application vars: application_sync_files: false application_compose_recreate: "always" application_remove_orphans: true - name: Get deployed image information shell: | docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown" args: executable: /bin/bash register: deployed_image changed_when: false - name: Record deployment metadata copy: content: | Deployment Timestamp: {{ deployment_timestamp }} Git Commit: {{ git_commit_sha }} Image Tag: {{ image_tag }} Deployed Image: {{ deployed_image.stdout }} Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }} Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }} Health Status: {{ application_health_output if application_health_output != '' else 'All services healthy' }} Health Check HTTP Status: {{ application_healthcheck_status }} dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt" owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0644' - name: Cleanup old backups (keep last {{ max_rollback_versions }}) shell: | cd {{ backups_path }} ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf args: executable: /bin/bash changed_when: false ignore_errors: yes post_tasks: - name: Display deployment summary debug: msg: - "=== Deployment Summary ===" - "Image: {{ app_image }}:{{ image_tag }}" - "Commit: {{ git_commit_sha }}" - "Timestamp: {{ deployment_timestamp }}" - "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}" - "Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}" - "Health Output: {{ application_health_output if application_health_output != '' else 'All services healthy' }}" - "Health Check HTTP Status: {{ application_healthcheck_status }}" - "Health Check URL: {{ health_check_url }}" - "" - "Next: Verify application is healthy"