feat: add system maintenance automation
This commit is contained in:
6
Makefile
6
Makefile
@@ -215,6 +215,10 @@ logs-production: ## Show production logs
|
|||||||
@echo "📋 Showing production logs..."
|
@echo "📋 Showing production logs..."
|
||||||
@cd deployment && make logs-prod-php
|
@cd deployment && make logs-prod-php
|
||||||
|
|
||||||
|
logs-staging: ## Show staging-app container logs via SSH
|
||||||
|
@echo "📋 Showing staging-app logs..."
|
||||||
|
@ssh -i ~/.ssh/production deploy@94.16.110.151 "cd ~/deployment/stacks/staging && docker compose logs -f staging-app"
|
||||||
|
|
||||||
# SSL Certificate Management (PHP Framework Integration)
|
# SSL Certificate Management (PHP Framework Integration)
|
||||||
ssl-init: ## Initialize Let's Encrypt certificates
|
ssl-init: ## Initialize Let's Encrypt certificates
|
||||||
@echo "🔒 Initializing SSL certificates..."
|
@echo "🔒 Initializing SSL certificates..."
|
||||||
@@ -249,4 +253,4 @@ ssl-backup: ## Backup Let's Encrypt certificates
|
|||||||
push-staging: ## Pusht den aktuellen Stand nach origin/staging
|
push-staging: ## Pusht den aktuellen Stand nach origin/staging
|
||||||
git push origin HEAD:staging
|
git push origin HEAD:staging
|
||||||
|
|
||||||
.PHONY: up down build restart logs ps phpinfo deploy setup clean clean-coverage status fix-ssh-perms setup-ssh test test-coverage test-coverage-html test-unit test-framework test-domain test-watch test-parallel test-profile test-filter security-check security-audit-json security-check-prod update-production restart-production deploy-production-quick status-production logs-production ssl-init ssl-init-staging ssl-test ssl-renew ssl-status ssl-backup push-staging
|
.PHONY: up down build restart logs ps phpinfo deploy setup clean clean-coverage status fix-ssh-perms setup-ssh test test-coverage test-coverage-html test-unit test-framework test-domain test-watch test-parallel test-profile test-filter security-check security-audit-json security-check-prod update-production restart-production deploy-production-quick status-production logs-production logs-staging ssl-init ssl-init-staging ssl-test ssl-renew ssl-status ssl-backup push-staging
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
# Production Deployment - Centralized Variables
|
# Production Deployment - Centralized Variables
|
||||||
# These variables are used across all playbooks
|
# These variables are used across all playbooks
|
||||||
|
|
||||||
|
# System Maintenance
|
||||||
|
system_update_packages: true
|
||||||
|
system_apt_upgrade: dist
|
||||||
|
system_enable_unattended_upgrades: true
|
||||||
|
system_enable_unattended_reboot: false
|
||||||
|
system_unattended_reboot_time: "02:00"
|
||||||
|
system_enable_unattended_timer: true
|
||||||
|
system_enable_docker_prune: false
|
||||||
|
|
||||||
# Deployment Paths
|
# Deployment Paths
|
||||||
deploy_user_home: "/home/deploy"
|
deploy_user_home: "/home/deploy"
|
||||||
stacks_base_path: "{{ deploy_user_home }}/deployment/stacks"
|
stacks_base_path: "{{ deploy_user_home }}/deployment/stacks"
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(docker_registry_username_default)) }}"
|
docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(docker_registry_username_default)) }}"
|
||||||
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(docker_registry_password_default)) }}"
|
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(docker_registry_password_default)) }}"
|
||||||
|
|
||||||
|
- name: Ensure system packages are up to date
|
||||||
|
include_role:
|
||||||
|
name: system
|
||||||
|
when: system_update_packages | bool
|
||||||
|
|
||||||
- name: Verify Docker is running
|
- name: Verify Docker is running
|
||||||
systemd:
|
systemd:
|
||||||
name: docker
|
name: docker
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
msg: "Deployment stacks directory not found at {{ stacks_base_path }}"
|
msg: "Deployment stacks directory not found at {{ stacks_base_path }}"
|
||||||
when: not stacks_dir.stat.exists
|
when: not stacks_dir.stat.exists
|
||||||
|
|
||||||
|
- name: Ensure system packages are up to date
|
||||||
|
include_role:
|
||||||
|
name: system
|
||||||
|
when: system_update_packages | bool
|
||||||
|
|
||||||
# Create external networks required by all stacks
|
# Create external networks required by all stacks
|
||||||
- name: Create traefik-public network
|
- name: Create traefik-public network
|
||||||
community.docker.docker_network:
|
community.docker.docker_network:
|
||||||
|
|||||||
11
deployment/ansible/playbooks/system-maintenance.yml
Normal file
11
deployment/ansible/playbooks/system-maintenance.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
- name: Apply system maintenance on production hosts
|
||||||
|
hosts: production
|
||||||
|
gather_facts: yes
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Run system maintenance role
|
||||||
|
include_role:
|
||||||
|
name: system
|
||||||
|
when: system_update_packages | bool
|
||||||
@@ -79,6 +79,14 @@
|
|||||||
mode: '0644'
|
mode: '0644'
|
||||||
when: application_nginx_src.stat.exists
|
when: application_nginx_src.stat.exists
|
||||||
|
|
||||||
|
- name: Expose secrets for template rendering
|
||||||
|
set_fact:
|
||||||
|
db_password: "{{ application_db_password }}"
|
||||||
|
redis_password: "{{ application_redis_password }}"
|
||||||
|
db_username: "{{ db_user | default(db_user_default) }}"
|
||||||
|
db_name: "{{ db_name | default(db_name_default) }}"
|
||||||
|
no_log: yes
|
||||||
|
|
||||||
- name: Render application environment file
|
- name: Render application environment file
|
||||||
template:
|
template:
|
||||||
src: "{{ application_env_template }}"
|
src: "{{ application_env_template }}"
|
||||||
@@ -86,9 +94,3 @@
|
|||||||
owner: "{{ ansible_user }}"
|
owner: "{{ ansible_user }}"
|
||||||
group: "{{ ansible_user }}"
|
group: "{{ ansible_user }}"
|
||||||
mode: '0600'
|
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 }}"
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ minio_wait_timeout: "{{ wait_timeout | default(60) }}"
|
|||||||
minio_wait_interval: 5
|
minio_wait_interval: 5
|
||||||
minio_env_template: "{{ role_path }}/../../templates/minio.env.j2"
|
minio_env_template: "{{ role_path }}/../../templates/minio.env.j2"
|
||||||
minio_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
|
minio_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
|
||||||
|
minio_healthcheck_enabled: false
|
||||||
|
|||||||
@@ -77,14 +77,18 @@
|
|||||||
register: minio_health_check
|
register: minio_health_check
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: not ansible_check_mode
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- minio_healthcheck_enabled | bool
|
||||||
|
|
||||||
- name: Display MinIO status
|
- name: Display MinIO status
|
||||||
debug:
|
debug:
|
||||||
msg: "MinIO health check: {{ 'SUCCESS' if minio_health_check.status == 200 else 'FAILED - Status: ' + (minio_health_check.status|string) }}"
|
msg: "MinIO health check: {{ 'SUCCESS' if minio_health_check.status == 200 else 'FAILED - Status: ' + (minio_health_check.status|string) }}"
|
||||||
when: not ansible_check_mode
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- minio_healthcheck_enabled | bool
|
||||||
|
|
||||||
- name: Record MinIO deployment facts
|
- name: Record MinIO deployment facts
|
||||||
set_fact:
|
set_fact:
|
||||||
minio_stack_changed: "{{ minio_compose_result.changed | default(false) }}"
|
minio_stack_changed: "{{ minio_compose_result.changed | default(false) }}"
|
||||||
minio_health_status: "{{ minio_health_check.status | default('unknown') }}"
|
minio_health_status: "{{ minio_health_check.status | default('disabled' if not minio_healthcheck_enabled else 'unknown') }}"
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ registry_stack_path: "{{ stacks_base_path }}/registry"
|
|||||||
registry_wait_timeout: "{{ wait_timeout | default(60) }}"
|
registry_wait_timeout: "{{ wait_timeout | default(60) }}"
|
||||||
registry_wait_interval: 5
|
registry_wait_interval: 5
|
||||||
registry_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
|
registry_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
|
||||||
|
registry_healthcheck_enabled: true
|
||||||
|
registry_healthcheck_url: "http://127.0.0.1:5000/v2/_catalog"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
- name: Verify Registry is accessible
|
- name: Verify Registry is accessible
|
||||||
uri:
|
uri:
|
||||||
url: "http://127.0.0.1:5000/v2/_catalog"
|
url: "{{ registry_healthcheck_url }}"
|
||||||
user: "{{ registry_username }}"
|
user: "{{ registry_username }}"
|
||||||
password: "{{ registry_password }}"
|
password: "{{ registry_password }}"
|
||||||
status_code: 200
|
status_code: 200
|
||||||
@@ -102,14 +102,18 @@
|
|||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
changed_when: false
|
changed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
when: not ansible_check_mode
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- registry_healthcheck_enabled | bool
|
||||||
|
|
||||||
- name: Display Registry status
|
- name: Display Registry status
|
||||||
debug:
|
debug:
|
||||||
msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}"
|
msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}"
|
||||||
when: not ansible_check_mode
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- registry_healthcheck_enabled | bool
|
||||||
|
|
||||||
- name: Record registry deployment facts
|
- name: Record registry deployment facts
|
||||||
set_fact:
|
set_fact:
|
||||||
registry_stack_changed: "{{ registry_compose_result.changed | default(false) }}"
|
registry_stack_changed: "{{ registry_compose_result.changed | default(false) }}"
|
||||||
registry_access_status: "{{ registry_check.status | default('unknown') }}"
|
registry_access_status: "{{ registry_check.status | default('disabled' if not registry_healthcheck_enabled else 'unknown') }}"
|
||||||
|
|||||||
9
deployment/ansible/roles/system/defaults/main.yml
Normal file
9
deployment/ansible/roles/system/defaults/main.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
system_update_packages: true
|
||||||
|
system_apt_cache_valid_time: 3600
|
||||||
|
system_apt_upgrade: dist
|
||||||
|
system_enable_unattended_upgrades: true
|
||||||
|
system_enable_unattended_reboot: false
|
||||||
|
system_unattended_reboot_time: "02:00"
|
||||||
|
system_enable_unattended_timer: true
|
||||||
|
system_enable_docker_prune: false
|
||||||
130
deployment/ansible/roles/system/tasks/main.yml
Normal file
130
deployment/ansible/roles/system/tasks/main.yml
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
- name: Refresh apt cache on Debian-based systems
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: yes
|
||||||
|
cache_valid_time: "{{ system_apt_cache_valid_time }}"
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_update_packages | bool
|
||||||
|
|
||||||
|
- name: Upgrade packages on Debian-based systems
|
||||||
|
ansible.builtin.apt:
|
||||||
|
upgrade: "{{ system_apt_upgrade }}"
|
||||||
|
autoremove: yes
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_update_packages | bool
|
||||||
|
|
||||||
|
- name: Upgrade packages on RedHat-based systems
|
||||||
|
ansible.builtin.yum:
|
||||||
|
name: '*'
|
||||||
|
state: latest
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'RedHat'
|
||||||
|
- system_update_packages | bool
|
||||||
|
|
||||||
|
- name: Warn about unsupported package manager
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "System package updates are not implemented for {{ ansible_os_family }}"
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- system_update_packages | bool
|
||||||
|
- ansible_os_family not in ['Debian', 'RedHat']
|
||||||
|
|
||||||
|
- name: Install unattended-upgrades packages
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- unattended-upgrades
|
||||||
|
- apt-listchanges
|
||||||
|
state: present
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_enable_unattended_upgrades | bool
|
||||||
|
|
||||||
|
- name: Configure unattended upgrades periodic execution
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
|
APT::Periodic::AutocleanInterval "7";
|
||||||
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_enable_unattended_upgrades | bool
|
||||||
|
|
||||||
|
- name: Configure unattended upgrade reboot preference
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||||
|
regexp: '^//?\s*Unattended-Upgrade::Automatic-Reboot\s+'
|
||||||
|
line: 'Unattended-Upgrade::Automatic-Reboot "{{ system_enable_unattended_reboot | ternary("true", "false") }}";'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
create: yes
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_enable_unattended_upgrades | bool
|
||||||
|
|
||||||
|
- name: Configure unattended upgrade reboot time
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||||
|
regexp: '^//?\s*Unattended-Upgrade::Automatic-Reboot-Time\s+'
|
||||||
|
line: 'Unattended-Upgrade::Automatic-Reboot-Time "{{ system_unattended_reboot_time }}";'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
create: yes
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_enable_unattended_upgrades | bool
|
||||||
|
- system_enable_unattended_reboot | bool
|
||||||
|
|
||||||
|
- name: Disable unattended reboot time when automatic reboot is off
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||||
|
regexp: '^Unattended-Upgrade::Automatic-Reboot-Time\s+'
|
||||||
|
state: absent
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
become: yes
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_enable_unattended_upgrades | bool
|
||||||
|
- not system_enable_unattended_reboot | bool
|
||||||
|
|
||||||
|
- name: Ensure unattended upgrade timers are enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "{{ item }}"
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
become: yes
|
||||||
|
loop:
|
||||||
|
- apt-daily.timer
|
||||||
|
- apt-daily-upgrade.timer
|
||||||
|
- unattended-upgrades.service
|
||||||
|
when:
|
||||||
|
- ansible_os_family == 'Debian'
|
||||||
|
- system_enable_unattended_upgrades | bool
|
||||||
|
- system_enable_unattended_timer | bool
|
||||||
|
|
||||||
|
- name: Prune unused Docker data
|
||||||
|
community.docker.docker_prune:
|
||||||
|
containers: true
|
||||||
|
images: true
|
||||||
|
networks: true
|
||||||
|
volumes: false
|
||||||
|
builder_cache: true
|
||||||
|
become: yes
|
||||||
|
when: system_enable_docker_prune | bool
|
||||||
@@ -16,6 +16,10 @@ APP_URL=https://{{ app_domain }}
|
|||||||
# Using PostgreSQL from postgres stack
|
# Using PostgreSQL from postgres stack
|
||||||
DB_HOST=postgres
|
DB_HOST=postgres
|
||||||
DB_PORT={{ db_port | default('5432') }}
|
DB_PORT={{ db_port | default('5432') }}
|
||||||
|
DB_DATABASE={{ db_name | default(db_name_default) }}
|
||||||
|
DB_USERNAME={{ db_user | default(db_user_default) }}
|
||||||
|
DB_PASSWORD={{ db_password }}
|
||||||
|
# Legacy variables (kept for backward compatibility)
|
||||||
DB_NAME={{ db_name | default(db_name_default) }}
|
DB_NAME={{ db_name | default(db_name_default) }}
|
||||||
DB_USER={{ db_user | default(db_user_default) }}
|
DB_USER={{ db_user | default(db_user_default) }}
|
||||||
DB_PASS={{ db_password }}
|
DB_PASS={{ db_password }}
|
||||||
|
|||||||
@@ -289,12 +289,16 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
app-code:
|
app-code:
|
||||||
name: app-code
|
name: app-code
|
||||||
|
external: true
|
||||||
app-storage:
|
app-storage:
|
||||||
name: app-storage
|
name: app-storage
|
||||||
|
external: true
|
||||||
app-logs:
|
app-logs:
|
||||||
name: app-logs
|
name: app-logs
|
||||||
|
external: true
|
||||||
redis-data:
|
redis-data:
|
||||||
name: redis-data
|
name: redis-data
|
||||||
|
external: true
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-public:
|
traefik-public:
|
||||||
|
|||||||
Reference in New Issue
Block a user