- Add pre-flight checks in deploy-update.yml - Automatically copy docker-compose.yml and nginx config in setup-infrastructure.yml - Add comprehensive deployment documentation - Ready for automated code deployments via CI/CD pipeline
425 lines
15 KiB
YAML
425 lines
15 KiB
YAML
---
|
|
- 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: Check if application stack docker-compose.yml exists locally
|
|
stat:
|
|
path: "{{ playbook_dir }}/../../stacks/application/docker-compose.yml"
|
|
register: app_compose_local
|
|
delegate_to: localhost
|
|
become: no
|
|
|
|
- name: Copy application stack docker-compose.yml to server
|
|
copy:
|
|
src: "{{ playbook_dir }}/../../stacks/application/docker-compose.yml"
|
|
dest: "{{ stacks_base_path }}/application/docker-compose.yml"
|
|
owner: "{{ ansible_user }}"
|
|
group: "{{ ansible_user }}"
|
|
mode: '0644'
|
|
when: app_compose_local.stat.exists
|
|
|
|
- name: Check if application stack nginx directory exists locally
|
|
stat:
|
|
path: "{{ playbook_dir }}/../../stacks/application/nginx/"
|
|
register: app_nginx_local
|
|
delegate_to: localhost
|
|
become: no
|
|
|
|
- name: Copy application stack nginx configuration to server
|
|
copy:
|
|
src: "{{ playbook_dir }}/../../stacks/application/nginx/"
|
|
dest: "{{ stacks_base_path }}/application/nginx/"
|
|
owner: "{{ ansible_user }}"
|
|
group: "{{ ansible_user }}"
|
|
mode: '0644'
|
|
when: app_nginx_local.stat.exists
|
|
|
|
- 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') }}"
|