--- - name: Deploy Infrastructure Stacks on Production Server hosts: production become: no gather_facts: yes vars: stacks_base_path: "~/deployment/stacks" wait_timeout: 60 tasks: - name: Check if deployment stacks directory exists stat: path: "{{ stacks_base_path }}" register: stacks_dir - name: Fail if stacks directory doesn't exist fail: msg: "Deployment stacks directory not found at {{ stacks_base_path }}" when: not stacks_dir.stat.exists # Create external networks required by all stacks - name: Create traefik-public network community.docker.docker_network: name: traefik-public driver: bridge state: present - name: Create app-internal network community.docker.docker_network: name: app-internal driver: bridge state: present # 1. Deploy Traefik (Reverse Proxy & SSL) - name: Deploy Traefik stack community.docker.docker_compose_v2: project_src: "{{ stacks_base_path }}/traefik" state: present pull: always register: traefik_output - name: Wait for Traefik to be ready wait_for: timeout: "{{ wait_timeout }}" when: traefik_output.changed - name: Check Traefik logs for readiness hint shell: docker compose logs traefik 2>&1 | grep -Ei "(configuration loaded|traefik[[:space:]]v|Starting provider|Server configuration loaded)" || true args: chdir: "{{ stacks_base_path }}/traefik" register: traefik_logs until: traefik_logs.stdout != "" retries: 6 delay: 10 changed_when: false ignore_errors: yes # 2. Deploy PostgreSQL (Database) - name: Deploy PostgreSQL stack community.docker.docker_compose_v2: project_src: "{{ stacks_base_path }}/postgresql" state: present pull: always register: postgres_output - name: Wait for PostgreSQL to be ready wait_for: timeout: "{{ wait_timeout }}" when: postgres_output.changed - name: Check PostgreSQL logs for readiness shell: docker compose logs postgres 2>&1 | grep -Ei "(ready to accept connections|database system is ready)" || true args: chdir: "{{ stacks_base_path }}/postgresql" register: postgres_logs until: postgres_logs.stdout != "" retries: 6 delay: 10 changed_when: false ignore_errors: yes # 3. Deploy Docker Registry (Private Registry) - name: Ensure Registry auth directory exists file: path: "{{ stacks_base_path }}/registry/auth" state: directory mode: '0755' become: yes - name: Create Registry htpasswd file if missing shell: | if [ ! -f {{ stacks_base_path }}/registry/auth/htpasswd ]; then docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin registry-secure-password-2025 > {{ stacks_base_path }}/registry/auth/htpasswd chmod 644 {{ stacks_base_path }}/registry/auth/htpasswd fi args: executable: /bin/bash become: yes changed_when: true register: registry_auth_created - name: Deploy Docker Registry stack community.docker.docker_compose_v2: project_src: "{{ stacks_base_path }}/registry" state: present pull: always register: registry_output - name: Wait for Docker Registry to be ready wait_for: timeout: "{{ wait_timeout }}" when: registry_output.changed - name: Check Registry logs for readiness shell: docker compose logs registry 2>&1 | grep -Ei "(listening on|listening at|http server)" || true args: chdir: "{{ stacks_base_path }}/registry" register: registry_logs until: registry_logs.stdout != "" retries: 6 delay: 10 changed_when: false ignore_errors: yes - name: Verify Registry is accessible uri: url: "http://127.0.0.1:5000/v2/_catalog" user: admin password: registry-secure-password-2025 status_code: 200 timeout: 5 register: registry_check ignore_errors: yes changed_when: false - name: Display Registry status debug: msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}" # 4. Deploy Gitea (CRITICAL - Git Server + MySQL + Redis) - name: Deploy Gitea stack community.docker.docker_compose_v2: project_src: "{{ stacks_base_path }}/gitea" state: present pull: always register: gitea_output - name: Wait for Gitea to be ready wait_for: timeout: "{{ wait_timeout }}" when: gitea_output.changed - name: Check Gitea logs for readiness shell: docker compose logs gitea 2>&1 | grep -Ei "(Listen:|Server is running|Starting server)" || true args: chdir: "{{ stacks_base_path }}/gitea" register: gitea_logs until: gitea_logs.stdout != "" retries: 12 delay: 10 changed_when: false ignore_errors: yes # 5. Deploy Monitoring (Portainer + Grafana + Prometheus) - name: Optionally load monitoring secrets from vault include_vars: file: "{{ playbook_dir }}/../secrets/production.vault.yml" no_log: yes ignore_errors: yes delegate_to: localhost become: no - name: Set Grafana admin password from vault or generate set_fact: grafana_admin_password: "{{ vault_grafana_admin_password | default(lookup('password', '/dev/null length=25 chars=ascii_letters,digits')) }}" - name: Set Prometheus password from vault or generate set_fact: prometheus_password: "{{ vault_prometheus_password | default(lookup('password', '/dev/null length=25 chars=ascii_letters,digits')) }}" - name: Generate Prometheus BasicAuth hash shell: | docker run --rm httpd:alpine htpasswd -nbB admin "{{ prometheus_password }}" 2>/dev/null | cut -d ":" -f 2 register: prometheus_auth_hash changed_when: false no_log: yes - name: Set Prometheus BasicAuth string set_fact: prometheus_auth: "admin:{{ prometheus_auth_hash.stdout }}" - name: Ensure monitoring stack directory exists file: path: "{{ stacks_base_path }}/monitoring" state: directory mode: '0755' - name: Create monitoring stack .env file template: src: "{{ playbook_dir }}/../templates/monitoring.env.j2" dest: "{{ stacks_base_path }}/monitoring/.env" owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0600' no_log: yes - name: Deploy Monitoring stack community.docker.docker_compose_v2: project_src: "{{ stacks_base_path }}/monitoring" state: present pull: always register: monitoring_output - name: Wait for Monitoring to be ready wait_for: timeout: "{{ wait_timeout }}" when: monitoring_output.changed # Verification - name: List all running containers command: > docker ps --format 'table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}' register: docker_ps_output - name: Display running containers debug: msg: "{{ docker_ps_output.stdout_lines }}" - name: Verify Gitea accessibility via HTTPS uri: url: https://git.michaelschiemer.de method: GET validate_certs: no status_code: 200 timeout: 10 register: gitea_http_check ignore_errors: yes - name: Display Gitea accessibility status debug: msg: "Gitea HTTPS check: {{ 'SUCCESS' if gitea_http_check.status == 200 else 'FAILED - Status: ' + (gitea_http_check.status|string) }}" # 6. Deploy Application Stack - name: Optionally load application secrets from vault include_vars: file: "{{ playbook_dir }}/../secrets/production.vault.yml" no_log: yes ignore_errors: yes delegate_to: localhost become: no - name: Check if PostgreSQL .env exists stat: path: "{{ stacks_base_path }}/postgresql/.env" register: postgres_env_file changed_when: false - name: Extract PostgreSQL password from .env file shell: "grep '^POSTGRES_PASSWORD=' {{ stacks_base_path }}/postgresql/.env 2>/dev/null | cut -d'=' -f2- || echo ''" register: postgres_password_from_file changed_when: false failed_when: false when: postgres_env_file.stat.exists no_log: yes - name: Set application database password (from file, vault, or generate) set_fact: app_db_password: "{{ postgres_password_from_file.stdout if (postgres_env_file.stat.exists and postgres_password_from_file.stdout != '') else (vault_db_root_password | default(lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation'))) }}" no_log: yes - name: Set application redis password from vault or generate set_fact: app_redis_password: "{{ vault_redis_password | default(lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation')) }}" - name: Ensure application stack directory exists file: path: "{{ stacks_base_path }}/application" state: directory mode: '0755' - name: Create application stack .env file template: src: "{{ playbook_dir }}/../templates/application.env.j2" dest: "{{ stacks_base_path }}/application/.env" owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0600' vars: db_password: "{{ app_db_password }}" db_user: "{{ db_user | default('postgres') }}" db_name: "{{ db_name | default('michaelschiemer') }}" redis_password: "{{ app_redis_password }}" app_domain: "{{ app_domain | default('michaelschiemer.de') }}" no_log: yes - name: Deploy Application stack community.docker.docker_compose_v2: project_src: "{{ stacks_base_path }}/application" state: present pull: always register: application_output - name: Wait for Application to be ready wait_for: timeout: "{{ wait_timeout }}" when: application_output.changed - name: Wait for application containers to be healthy pause: seconds: 30 when: application_output.changed - name: Check application container health status shell: | docker compose -f {{ stacks_base_path }}/application/docker-compose.yml ps --format json | jq -r '.[] | select(.Health != "healthy" and .Health != "" and .Health != "starting") | "\(.Name): \(.Health)"' || echo "All healthy or no health checks" args: executable: /bin/bash register: app_health_status changed_when: false ignore_errors: yes - name: Display application health status debug: msg: "Application health: {{ app_health_status.stdout if app_health_status.stdout != '' else 'All services healthy or starting' }}" - name: Wait for app container to be ready before migration wait_for: timeout: 60 when: application_output.changed - name: Check if app container is running shell: | docker compose -f {{ stacks_base_path }}/application/docker-compose.yml ps app | grep -q "Up" || exit 1 args: executable: /bin/bash register: app_container_running changed_when: false failed_when: false when: application_output.changed - name: Run database migrations shell: | docker compose -f {{ stacks_base_path }}/application/docker-compose.yml exec -T app php console.php db:migrate args: executable: /bin/bash register: migration_result changed_when: true failed_when: false ignore_errors: yes when: application_output.changed and app_container_running.rc == 0 - name: Display migration result debug: msg: | Migration Result: {{ migration_result.stdout if migration_result.rc == 0 else 'Migration may have failed - check logs with: docker compose -f ' + stacks_base_path + '/application/docker-compose.yml logs app' }} when: application_output.changed - name: Verify application accessibility via HTTPS uri: url: "https://{{ app_domain | default('michaelschiemer.de') }}/health" method: GET validate_certs: no status_code: [200, 404, 502, 503] timeout: 10 register: app_health_check ignore_errors: yes when: application_output.changed - name: Display application accessibility status debug: msg: "Application health check: {{ 'SUCCESS (HTTP ' + (app_health_check.status|string) + ')' if app_health_check.status == 200 else 'FAILED or not ready yet (HTTP ' + (app_health_check.status|string) + ')' }}" when: application_output.changed - name: Summary debug: msg: - "=== Infrastructure Deployment Complete ===" - "Traefik: {{ 'Deployed' if traefik_output.changed else 'Already running' }}" - "PostgreSQL: {{ 'Deployed' if postgres_output.changed else 'Already running' }}" - "Docker Registry: {{ 'Deployed' if registry_output.changed else 'Already running' }}" - "Gitea: {{ 'Deployed' if gitea_output.changed else 'Already running' }}" - "Monitoring: {{ 'Deployed' if monitoring_output.changed else 'Already running' }}" - "Application: {{ 'Deployed' if application_output.changed else 'Already running' }}" - "" - "Next Steps:" - "1. Access Gitea at: https://git.michaelschiemer.de" - "2. Complete Gitea setup wizard if first-time deployment" - "3. Navigate to Admin > Actions > Runners to get registration token" - "4. Continue with Phase 1 - Gitea Runner Setup" - "5. Access Application at: https://{{ app_domain | default('michaelschiemer.de') }}"