Files
michaelschiemer/deployment/ansible/playbooks/setup-infrastructure.yml
Michael Schiemer 16d586ecdf chore: Update deployment configuration and documentation
- Update Gitea configuration (remove DEFAULT_ACTIONS_URL)
- Fix deployment documentation
- Update Ansible playbooks
- Clean up deprecated files
- Add new deployment scripts and templates
2025-10-31 21:11:11 +01:00

529 lines
18 KiB
YAML

---
- name: Deploy Infrastructure Stacks on Production Server
hosts: production
become: no
gather_facts: yes
vars:
# All deployment variables are now defined in group_vars/production.yml
# Variables can be overridden via -e flag if needed
tasks:
- name: Debug - Show variables
debug:
msg:
- "stacks_base_path: {{ stacks_base_path | default('NOT SET') }}"
- "deploy_user_home: {{ deploy_user_home | default('NOT SET') }}"
when: false # Only enable for debugging
- 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: "{{ registry_auth_path }}"
state: directory
mode: '0755'
become: yes
- name: Optionally load registry credentials from vault
include_vars:
file: "{{ playbook_dir }}/../secrets/production.vault.yml"
no_log: yes
ignore_errors: yes
delegate_to: localhost
become: no
- name: Set registry credentials from vault or defaults
set_fact:
registry_username: "{{ vault_docker_registry_username | default(docker_registry_username_default) }}"
registry_password: "{{ vault_docker_registry_password | default(docker_registry_password_default) }}"
no_log: true
- name: Fail if registry password is not set
fail:
msg: "Registry password must be set in vault or docker_registry_password_default"
when: registry_password is not defined or registry_password == ""
- name: Create Registry htpasswd file if missing
shell: |
if [ ! -f {{ registry_auth_path }}/htpasswd ]; then
docker run --rm --entrypoint htpasswd httpd:2 -Bbn {{ registry_username }} {{ registry_password }} > {{ registry_auth_path }}/htpasswd
chmod 644 {{ registry_auth_path }}/htpasswd
fi
args:
executable: /bin/bash
become: yes
changed_when: true
register: registry_auth_created
no_log: true
- 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: "{{ registry_username }}"
password: "{{ registry_password }}"
status_code: 200
timeout: 5
register: registry_check
ignore_errors: yes
changed_when: false
no_log: true
- name: Display Registry status
debug:
msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}"
# 4. Deploy MinIO (Object Storage)
- name: Optionally load MinIO 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 MinIO root password from vault or generate
set_fact:
minio_password: "{{ vault_minio_root_password | default(lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation')) }}"
no_log: yes
- name: Set MinIO root user from vault or use default
set_fact:
minio_user: "{{ vault_minio_root_user | default('minioadmin') }}"
- name: Ensure MinIO stack directory exists
file:
path: "{{ stacks_base_path }}/minio"
state: directory
mode: '0755'
- name: Create MinIO stack .env file
template:
src: "{{ playbook_dir }}/../templates/minio.env.j2"
dest: "{{ stacks_base_path }}/minio/.env"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
vars:
minio_root_user: "{{ minio_user }}"
minio_root_password: "{{ minio_password }}"
minio_api_domain: "{{ minio_api_domain }}"
minio_console_domain: "{{ minio_console_domain }}"
no_log: yes
- name: Deploy MinIO stack
community.docker.docker_compose_v2:
project_src: "{{ stacks_base_path }}/minio"
state: present
pull: always
register: minio_output
- name: Wait for MinIO to be ready
wait_for:
timeout: "{{ wait_timeout }}"
when: minio_output.changed
- name: Check MinIO logs for readiness
shell: docker compose logs minio 2>&1 | grep -Ei "(API:|WebUI:|MinIO Object Storage Server)" || true
args:
chdir: "{{ stacks_base_path }}/minio"
register: minio_logs
until: minio_logs.stdout != ""
retries: 6
delay: 10
changed_when: false
ignore_errors: yes
- name: Verify MinIO health endpoint
uri:
url: "http://127.0.0.1:9000/minio/health/live"
method: GET
status_code: [200, 404, 502, 503]
timeout: 5
register: minio_health_check
ignore_errors: yes
changed_when: false
- name: Display MinIO status
debug:
msg: "MinIO health check: {{ 'SUCCESS' if minio_health_check.status == 200 else 'FAILED - Status: ' + (minio_health_check.status|string) }}"
# 5. 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
# 6. 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://{{ gitea_domain }}"
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) }}"
# 7. 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(db_user_default) }}"
db_name: "{{ db_name | default(db_name_default) }}"
redis_password: "{{ app_redis_password }}"
app_domain: "{{ app_domain }}"
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: "{{ health_check_url }}"
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' }}"
- "MinIO: {{ 'Deployed' if minio_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://{{ gitea_domain }}"
- "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 }}"