--- # Production Container Deployment Playbook # Deploys pre-built container images for Custom PHP Framework - name: Deploy Custom PHP Framework Application hosts: web_servers become: true gather_facts: true vars: app_path: "/var/www/html" backup_path: "/var/www/backups" image_tag: "{{ IMAGE_TAG | default('latest') }}" domain_name: "{{ DOMAIN_NAME | default('michaelschiemer.de') }}" backup_enabled: "{{ BACKUP_ENABLED | default(true) | bool }}" backup_retention_days: "{{ BACKUP_RETENTION_DAYS | default(30) }}" cdn_update: "{{ CDN_UPDATE | default(false) | bool }}" # Pfade für Templates/Compose relativ zum Playbook-Verzeichnis compose_base_src: "{{ playbook_dir }}/../../../docker-compose.yml" compose_overlay_src: "{{ playbook_dir }}/../../applications/docker-compose.{{ environment }}.yml" env_template_src: "{{ playbook_dir }}/../../applications/environments/.env.{{ environment }}.template" # Compose-Projektname: Standardmäßig Verzeichnisname von app_path (z. B. 'html') compose_project: "{{ compose_project_name | default(app_path | basename) }}" pre_tasks: - name: Verify deployment requirements assert: that: - app_path is defined - domain_name is defined - image_tag is defined - image_tag != 'latest' or environment != 'production' fail_msg: "Production deployment requires specific image tag (not 'latest')" tags: always - name: Create required directories ansible.builtin.file: path: "{{ item }}" state: directory owner: deploy group: deploy mode: '0755' loop: - "{{ app_path }}" - "{{ backup_path }}" - /var/log/applications tags: always - name: Store current image tag for rollback ansible.builtin.shell: | if [ -f {{ app_path }}/.env.{{ environment }} ]; then grep '^IMAGE_TAG=' {{ app_path }}/.env.{{ environment }} | cut -d'=' -f2 > {{ app_path }}/.last_release || echo 'none' fi ignore_errors: true tags: backup tasks: - name: Check for existing deployment ansible.builtin.stat: path: "{{ app_path }}/docker-compose.yml" register: existing_deployment tags: deploy - name: Render environment file from template ansible.builtin.template: src: "{{ env_template_src }}" dest: "{{ app_path }}/.env.{{ environment }}" owner: deploy group: deploy mode: '0600' backup: true vars: IMAGE_TAG: "{{ image_tag }}" DOMAIN_NAME: "{{ domain_name }}" no_log: true tags: deploy - name: Copy Docker Compose files (base + overlay) ansible.builtin.copy: src: "{{ item.src }}" dest: "{{ app_path }}/{{ item.dest }}" owner: deploy group: deploy mode: '0644' loop: - { src: "{{ compose_base_src }}", dest: "docker-compose.yml" } - { src: "{{ compose_overlay_src }}", dest: "docker-compose.{{ environment }}.yml" } tags: deploy - name: Stop existing services gracefully if present community.docker.docker_compose_v2: project_src: "{{ app_path }}" files: - docker-compose.yml - "docker-compose.{{ environment }}.yml" env_files: - ".env.{{ environment }}" state: stopped timeout: 60 when: existing_deployment.stat.exists ignore_errors: true tags: deploy - name: Create storage volumes with proper permissions ansible.builtin.file: path: "{{ app_path }}/{{ item }}" state: directory owner: www-data group: www-data mode: '0775' loop: - storage - storage/logs - storage/cache - var - var/logs tags: deploy - name: Deploy application with Docker Compose v2 community.docker.docker_compose_v2: project_src: "{{ app_path }}" files: - docker-compose.yml - "docker-compose.{{ environment }}.yml" env_files: - ".env.{{ environment }}" pull: true build: false state: present recreate: smart remove_orphans: true timeout: 300 tags: deploy - name: Wait for PHP container to be healthy (label-based) community.docker.docker_container_info: filters: label: - "com.docker.compose.service=php" - "com.docker.compose.project={{ compose_project }}" register: php_info retries: 20 delay: 10 until: php_info.containers is defined and (php_info.containers | length) > 0 and ( (php_info.containers[0].State.Health is defined and php_info.containers[0].State.Health.Status == "healthy") or php_info.containers[0].State.Status == "running" ) tags: deploy - name: Run database migrations community.docker.docker_container_exec: container: "{{ php_info.containers[0].Id }}" command: php console.php db:migrate --force chdir: /var/www/html tags: deploy - name: Clear application caches community.docker.docker_container_exec: container: "{{ php_info.containers[0].Id }}" command: "php console.php {{ item }}" chdir: /var/www/html loop: - cache:clear - view:clear ignore_errors: true tags: deploy - name: Wait for application to be ready ansible.builtin.uri: url: "https://{{ domain_name }}/health" method: GET status_code: 200 timeout: 30 headers: User-Agent: "Mozilla/5.0 (Ansible Health Check)" validate_certs: true register: http_health retries: 15 delay: 10 until: http_health.status == 200 tags: deploy - name: Store successful deployment tag ansible.builtin.copy: content: "{{ image_tag }}" dest: "{{ app_path }}/.last_successful_release" owner: deploy group: deploy mode: '0644' tags: deploy post_tasks: - name: Clean up old backups ansible.builtin.find: paths: "{{ backup_path }}" age: "{{ backup_retention_days }}d" file_type: directory register: old_backups when: backup_enabled tags: cleanup - name: Remove old backup directories ansible.builtin.file: path: "{{ item.path }}" state: absent loop: "{{ old_backups.files }}" when: backup_enabled and old_backups.files is defined tags: cleanup - name: Import CDN update playbook if enabled import_playbook: update-cdn.yml when: cdn_update | default(false) | bool tags: cdn - name: Deployment success notification ansible.builtin.debug: msg: - "Application deployment completed successfully" - "Image Tag: {{ image_tag }}" - "Environment: {{ environment }}" - "Domain: {{ domain_name }}" - "CDN Updated: {{ cdn_update }}" tags: always