chore: sync staging workspace

This commit is contained in:
2025-11-01 19:02:09 +01:00
parent 478754ab02
commit 5a79646daf
58 changed files with 2035 additions and 709 deletions

View File

@@ -0,0 +1,34 @@
---
# Source path for application stack files on the control node
application_stack_src: "{{ role_path }}/../../stacks/application"
# Destination path on the target host (defaults to configured app_stack_path)
application_stack_dest: "{{ app_stack_path | default(stacks_base_path + '/application') }}"
# Template used to generate the application .env file
application_env_template: "{{ role_path }}/../../templates/application.env.j2"
# Optional vault file containing secrets (loaded if present)
application_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
# Whether to synchronize stack files from repository
application_sync_files: true
# Compose recreate strategy ("auto", "always", "never")
application_compose_recreate: "auto"
# Whether to remove orphaned containers during compose up
application_remove_orphans: false
# Whether to run database migrations after (re)deploying the stack
application_run_migrations: true
# Optional health check URL to verify after deployment
application_healthcheck_url: "{{ health_check_url | default('') }}"
# Timeout used for waits in this role
application_wait_timeout: "{{ wait_timeout | default(60) }}"
application_wait_interval: 5
# Command executed inside the app container to run migrations
application_migration_command: "php console.php db:migrate"

View File

@@ -0,0 +1,69 @@
---
- name: Deploy application stack
community.docker.docker_compose_v2:
project_src: "{{ application_stack_dest }}"
state: present
pull: always
recreate: "{{ application_compose_recreate }}"
remove_orphans: "{{ application_remove_orphans | bool }}"
register: application_compose_result
- name: Wait for application container to report Up
shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.yml ps app | grep -Eiq "Up|running"
register: application_app_running
changed_when: false
until: application_app_running.rc == 0
retries: "{{ ((application_wait_timeout | int) + (application_wait_interval | int) - 1) // (application_wait_interval | int) }}"
delay: "{{ application_wait_interval | int }}"
when: application_compose_result.changed
- name: Ensure app container is running before migrations
shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.yml ps app | grep -Eiq "Up|running"
args:
executable: /bin/bash
register: application_app_container_running
changed_when: false
failed_when: false
when: application_compose_result.changed
- name: Run database migrations
shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.yml exec -T app {{ application_migration_command }}
args:
executable: /bin/bash
register: application_migration_result
changed_when: true
failed_when: false
ignore_errors: yes
when:
- application_run_migrations
- application_compose_result.changed
- application_app_container_running.rc == 0
- name: Collect application container status
shell: docker compose -f {{ application_stack_dest }}/docker-compose.yml ps
register: application_ps
changed_when: false
ignore_errors: yes
- name: Perform application health check
uri:
url: "{{ application_healthcheck_url }}"
method: GET
validate_certs: no
status_code: [200, 404, 502, 503]
timeout: 10
register: application_healthcheck_result
ignore_errors: yes
when:
- application_healthcheck_url | length > 0
- application_compose_result.changed
- name: Set application role summary facts
set_fact:
application_stack_changed: "{{ application_compose_result.changed | default(false) }}"
application_health_output: "{{ application_ps.stdout | default('') }}"
application_healthcheck_status: "{{ application_healthcheck_result.status | default('unknown') }}"
application_migration_stdout: "{{ application_migration_result.stdout | default('') }}"

View File

@@ -0,0 +1,7 @@
---
- name: Synchronize application stack files
include_tasks: sync.yml
when: application_sync_files | bool
- name: Deploy application stack
include_tasks: deploy.yml

View File

@@ -0,0 +1,94 @@
---
- name: Ensure application stack destination directory exists
file:
path: "{{ application_stack_dest }}"
state: directory
mode: '0755'
- name: Check if vault file exists locally
stat:
path: "{{ application_vault_file }}"
delegate_to: localhost
register: application_vault_stat
become: no
- name: Optionally load application secrets from vault
include_vars:
file: "{{ application_vault_file }}"
when: application_vault_stat.stat.exists
no_log: yes
delegate_to: localhost
become: no
- name: Check if PostgreSQL .env exists on target host
stat:
path: "{{ stacks_base_path }}/postgresql/.env"
register: application_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: application_postgres_password
changed_when: false
failed_when: false
when: application_postgres_env_file.stat.exists
no_log: yes
- name: Determine application database password
set_fact:
application_db_password: >-
{{ (application_postgres_env_file.stat.exists and application_postgres_password.stdout != '') |
ternary(application_postgres_password.stdout,
vault_db_root_password | default(lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation'))) }}
no_log: yes
- name: Determine application redis password
set_fact:
application_redis_password: "{{ vault_redis_password | default(lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation')) }}"
no_log: yes
- name: Check if application docker-compose source exists locally
stat:
path: "{{ application_stack_src }}/docker-compose.yml"
delegate_to: localhost
register: application_compose_src
become: no
- name: Copy application docker-compose to target host
copy:
src: "{{ application_stack_src }}/docker-compose.yml"
dest: "{{ application_stack_dest }}/docker-compose.yml"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0644'
when: application_compose_src.stat.exists
- name: Check if nginx configuration exists locally
stat:
path: "{{ application_stack_src }}/nginx"
delegate_to: localhost
register: application_nginx_src
become: no
- name: Synchronize nginx configuration
copy:
src: "{{ application_stack_src }}/nginx/"
dest: "{{ application_stack_dest }}/nginx/"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0644'
when: application_nginx_src.stat.exists
- name: Render application environment file
template:
src: "{{ application_env_template }}"
dest: "{{ application_stack_dest }}/.env"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
vars:
db_password: "{{ application_db_password }}"
db_user: "{{ db_user | default(db_user_default) }}"
db_name: "{{ db_name | default(db_name_default) }}"
redis_password: "{{ application_redis_password }}"
app_domain: "{{ app_domain }}"

View File

@@ -0,0 +1,4 @@
---
gitea_stack_path: "{{ stacks_base_path }}/gitea"
gitea_wait_timeout: "{{ wait_timeout | default(60) }}"
gitea_wait_interval: 5

View File

@@ -0,0 +1,35 @@
---
- name: Deploy Gitea stack
community.docker.docker_compose_v2:
project_src: "{{ gitea_stack_path }}"
state: present
pull: always
register: gitea_compose_result
- name: Check Gitea container status
shell: |
docker compose -f {{ gitea_stack_path }}/docker-compose.yml ps gitea | grep -Eiq "Up|running"
register: gitea_state
changed_when: false
until: gitea_state.rc == 0
retries: "{{ ((gitea_wait_timeout | int) + (gitea_wait_interval | int) - 1) // (gitea_wait_interval | int) }}"
delay: "{{ gitea_wait_interval | int }}"
failed_when: gitea_state.rc != 0
when: not ansible_check_mode
- name: Check Gitea logs for readiness
shell: docker compose logs gitea 2>&1 | grep -Ei "(Listen:|Server is running|Starting server)" || true
args:
chdir: "{{ gitea_stack_path }}"
register: gitea_logs
until: gitea_logs.stdout != ""
retries: 12
delay: 10
changed_when: false
failed_when: false
when: not ansible_check_mode
- name: Record Gitea deployment facts
set_fact:
gitea_stack_changed: "{{ gitea_compose_result.changed | default(false) }}"
gitea_log_hint: "{{ gitea_logs.stdout | default('') }}"

View File

@@ -0,0 +1,6 @@
---
minio_stack_path: "{{ stacks_base_path }}/minio"
minio_wait_timeout: "{{ wait_timeout | default(60) }}"
minio_wait_interval: 5
minio_env_template: "{{ role_path }}/../../templates/minio.env.j2"
minio_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"

View File

@@ -0,0 +1,90 @@
---
- name: Check if MinIO vault file exists
stat:
path: "{{ minio_vault_file }}"
delegate_to: localhost
register: minio_vault_stat
become: no
- name: Optionally load MinIO secrets from vault
include_vars:
file: "{{ minio_vault_file }}"
when: minio_vault_stat.stat.exists
no_log: yes
delegate_to: localhost
become: no
- name: Set MinIO root password from vault or generate
set_fact:
minio_root_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_root_user: "{{ vault_minio_root_user | default('minioadmin') }}"
no_log: yes
- name: Ensure MinIO stack directory exists
file:
path: "{{ minio_stack_path }}"
state: directory
mode: '0755'
- name: Create MinIO stack .env file
template:
src: "{{ minio_env_template }}"
dest: "{{ minio_stack_path }}/.env"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
- name: Deploy MinIO stack
community.docker.docker_compose_v2:
project_src: "{{ minio_stack_path }}"
state: present
pull: always
register: minio_compose_result
- name: Check MinIO container status
shell: |
docker compose -f {{ minio_stack_path }}/docker-compose.yml ps minio | grep -Eiq "Up|running"
register: minio_state
changed_when: false
until: minio_state.rc == 0
retries: "{{ ((minio_wait_timeout | int) + (minio_wait_interval | int) - 1) // (minio_wait_interval | int) }}"
delay: "{{ minio_wait_interval | int }}"
failed_when: minio_state.rc != 0
when: not ansible_check_mode
- name: Check MinIO logs for readiness
shell: docker compose logs minio 2>&1 | grep -Ei "(API:|WebUI:|MinIO Object Storage Server)" || true
args:
chdir: "{{ minio_stack_path }}"
register: minio_logs
until: minio_logs.stdout != ""
retries: 6
delay: 10
changed_when: false
failed_when: false
when: not ansible_check_mode
- 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
when: not ansible_check_mode
- name: Display MinIO status
debug:
msg: "MinIO health check: {{ 'SUCCESS' if minio_health_check.status == 200 else 'FAILED - Status: ' + (minio_health_check.status|string) }}"
when: not ansible_check_mode
- name: Record MinIO deployment facts
set_fact:
minio_stack_changed: "{{ minio_compose_result.changed | default(false) }}"
minio_health_status: "{{ minio_health_check.status | default('unknown') }}"

View File

@@ -0,0 +1,5 @@
---
monitoring_stack_path: "{{ stacks_base_path }}/monitoring"
monitoring_wait_timeout: "{{ wait_timeout | default(60) }}"
monitoring_env_template: "{{ role_path }}/../../templates/monitoring.env.j2"
monitoring_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"

View File

@@ -0,0 +1,68 @@
---
- name: Check if monitoring vault file exists
stat:
path: "{{ monitoring_vault_file }}"
delegate_to: localhost
register: monitoring_vault_stat
become: no
- name: Optionally load monitoring secrets from vault
include_vars:
file: "{{ monitoring_vault_file }}"
when: monitoring_vault_stat.stat.exists
no_log: 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')) }}"
no_log: yes
- 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')) }}"
no_log: yes
- 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 }}"
no_log: yes
- name: Ensure monitoring stack directory exists
file:
path: "{{ monitoring_stack_path }}"
state: directory
mode: '0755'
- name: Create monitoring stack .env file
template:
src: "{{ monitoring_env_template }}"
dest: "{{ monitoring_stack_path }}/.env"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
no_log: yes
- name: Deploy Monitoring stack
community.docker.docker_compose_v2:
project_src: "{{ monitoring_stack_path }}"
state: present
pull: always
register: monitoring_compose_result
- name: Wait for Monitoring to be ready
wait_for:
timeout: "{{ monitoring_wait_timeout }}"
when: monitoring_compose_result.changed
- name: Record monitoring deployment facts
set_fact:
monitoring_stack_changed: "{{ monitoring_compose_result.changed | default(false) }}"

View File

@@ -0,0 +1,4 @@
---
postgresql_stack_path: "{{ stacks_base_path }}/postgresql"
postgresql_wait_timeout: "{{ wait_timeout | default(60) }}"
postgresql_wait_interval: 5

View File

@@ -0,0 +1,23 @@
---
- name: Deploy PostgreSQL stack
community.docker.docker_compose_v2:
project_src: "{{ postgresql_stack_path }}"
state: present
pull: always
register: postgresql_compose_result
- name: Check PostgreSQL container status
shell: |
docker compose -f {{ postgresql_stack_path }}/docker-compose.yml ps postgres | grep -Eiq "Up|running"
register: postgresql_state
changed_when: false
until: postgresql_state.rc == 0
retries: "{{ ((postgresql_wait_timeout | int) + (postgresql_wait_interval | int) - 1) // (postgresql_wait_interval | int) }}"
delay: "{{ postgresql_wait_interval | int }}"
failed_when: postgresql_state.rc != 0
when: not ansible_check_mode
- name: Record PostgreSQL deployment facts
set_fact:
postgresql_stack_changed: "{{ postgresql_compose_result.changed | default(false) }}"
postgresql_log_hint: ""

View File

@@ -0,0 +1,5 @@
---
registry_stack_path: "{{ stacks_base_path }}/registry"
registry_wait_timeout: "{{ wait_timeout | default(60) }}"
registry_wait_interval: 5
registry_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"

View File

@@ -0,0 +1,115 @@
---
- name: Ensure Registry auth directory exists
file:
path: "{{ registry_auth_path }}"
state: directory
mode: '0755'
become: yes
- name: Check if registry vault file exists
stat:
path: "{{ registry_vault_file }}"
delegate_to: localhost
register: registry_vault_stat
become: no
- name: Optionally load registry credentials from vault
include_vars:
file: "{{ registry_vault_file }}"
when: registry_vault_stat.stat.exists
no_log: yes
delegate_to: localhost
become: no
register: registry_vault_vars
failed_when: false
- name: Fail if registry vault decryption failed
fail:
msg: >
Failed to decrypt {{ registry_vault_file }}.
Provide a valid vault password (e.g. via --vault-password-file) or update docker_registry_password_default.
when:
- not ansible_check_mode
- registry_vault_stat.stat.exists
- registry_vault_vars is defined
- registry_vault_vars.failed | default(false)
- name: Set registry credentials from vault or defaults or generate
set_fact:
registry_username: "{{ vault_docker_registry_username | default(docker_registry_username_default) }}"
registry_password: >-
{{
vault_docker_registry_password
| default(docker_registry_password_default)
| default(lookup('password', '/dev/null length=32 chars=ascii_letters,digits'))
}}
no_log: true
- name: Create Registry htpasswd file if missing
shell: |
docker run --rm --entrypoint htpasswd httpd:2 -Bbn {{ registry_username }} {{ registry_password }} > {{ registry_auth_path }}/htpasswd
chmod 644 {{ registry_auth_path }}/htpasswd
args:
executable: /bin/bash
creates: "{{ registry_auth_path }}/htpasswd"
become: yes
no_log: true
when: not ansible_check_mode
- name: Deploy Docker Registry stack
community.docker.docker_compose_v2:
project_src: "{{ registry_stack_path }}"
state: present
pull: always
register: registry_compose_result
- name: Wait for Docker Registry to be ready
wait_for:
timeout: "{{ registry_wait_timeout }}"
when: registry_compose_result.changed
- name: Check Registry container status
shell: |
docker compose -f {{ registry_stack_path }}/docker-compose.yml ps registry | grep -Eiq "Up|running"
register: registry_state
changed_when: false
until: registry_state.rc == 0
retries: "{{ ((registry_wait_timeout | int) + (registry_wait_interval | int) - 1) // (registry_wait_interval | int) }}"
delay: "{{ registry_wait_interval | int }}"
failed_when: registry_state.rc != 0
when: not ansible_check_mode
- name: Check Registry logs for readiness
shell: docker compose logs registry 2>&1 | grep -Ei "(listening on|listening at|http server)" || true
args:
chdir: "{{ registry_stack_path }}"
register: registry_logs
until: registry_logs.stdout != ""
retries: 6
delay: 10
changed_when: false
failed_when: false
when: not ansible_check_mode
- 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
when: not ansible_check_mode
- name: Display Registry status
debug:
msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}"
when: not ansible_check_mode
- name: Record registry deployment facts
set_fact:
registry_stack_changed: "{{ registry_compose_result.changed | default(false) }}"
registry_access_status: "{{ registry_check.status | default('unknown') }}"

View File

@@ -0,0 +1,4 @@
---
traefik_stack_path: "{{ stacks_base_path }}/traefik"
traefik_wait_timeout: "{{ wait_timeout | default(60) }}"
traefik_wait_interval: 5

View File

@@ -0,0 +1,23 @@
---
- name: Deploy Traefik stack
community.docker.docker_compose_v2:
project_src: "{{ traefik_stack_path }}"
state: present
pull: always
register: traefik_compose_result
- name: Check Traefik container status
shell: |
docker compose -f {{ traefik_stack_path }}/docker-compose.yml ps traefik | grep -Eiq "Up|running"
register: traefik_state
changed_when: false
until: traefik_state.rc == 0
retries: "{{ ((traefik_wait_timeout | int) + (traefik_wait_interval | int) - 1) // (traefik_wait_interval | int) }}"
delay: "{{ traefik_wait_interval | int }}"
failed_when: traefik_state.rc != 0
when: not ansible_check_mode
- name: Record Traefik deployment facts
set_fact:
traefik_stack_changed: "{{ traefik_compose_result.changed | default(false) }}"
traefik_log_hint: ""