Initial commit
This commit is contained in:
97
tasks/backup.yaml
Normal file
97
tasks/backup.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
- name: "{{ app_name }} - Backup application data"
|
||||
block:
|
||||
- name: Starting backup
|
||||
debug:
|
||||
msg: |
|
||||
Starting backup of {{ app_name }}
|
||||
Backing up dirs: {{ app_backup_subdirectories }}
|
||||
|
||||
- name: Validate required backup variables
|
||||
ansible.builtin.fail:
|
||||
msg: "Required variable {{ item }} is not defined"
|
||||
when: vars[item] is not defined
|
||||
loop:
|
||||
- app_name
|
||||
- app_dir
|
||||
- backup_path
|
||||
|
||||
- name: Set backup source paths
|
||||
ansible.builtin.set_fact:
|
||||
_backup_sources: "{{ app_backup_subdirectories | default([app_dir + '/*']) }}"
|
||||
|
||||
- name: Create local backup directory on controller
|
||||
ansible.builtin.file:
|
||||
path: "{{ app_backup_path }}"
|
||||
state: directory
|
||||
mode: "0750"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create remote backup directory on host
|
||||
ansible.builtin.file:
|
||||
path: "{{ app_backup_path_remote | default(app_dir + '/backups') }}"
|
||||
state: directory
|
||||
owner: "{{ app_uid | default(omit) }}"
|
||||
group: "{{ app_gid | default(omit) }}"
|
||||
mode: "{{ app_permission_mode | default('0755') }}"
|
||||
|
||||
- name: Stop application for backup
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: stopped
|
||||
when: app_backup_stop_services | default(true)
|
||||
|
||||
- name: Create timestamped backup
|
||||
ansible.builtin.archive:
|
||||
path: "{{ _backup_sources }}"
|
||||
dest: "{{ app_backup_path_remote | default(app_dir + '/backups') }}/{{ app_name }}-{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
|
||||
format: gz
|
||||
owner: "{{ app_uid | default(omit) }}"
|
||||
group: "{{ app_gid | default(omit) }}"
|
||||
mode: "{{ app_permission_mode | default('0644') }}"
|
||||
register: remote_backup
|
||||
|
||||
- name: Copy backup to controller
|
||||
ansible.builtin.fetch:
|
||||
src: "{{ remote_backup.dest }}"
|
||||
dest: "{{ app_backup_path }}/{{ app_name }}-{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
|
||||
flat: true
|
||||
when: remote_backup is succeeded
|
||||
|
||||
- name: Clean old backups on controller
|
||||
ansible.builtin.find:
|
||||
paths: "{{ app_backup_path }}"
|
||||
patterns: "{{ app_name }}-*.tar.gz"
|
||||
age: "{{ app_backup_retention_days_controller | default(90) }}d"
|
||||
register: old_backups_controller
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Remove old backups from controller
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ old_backups_controller.files | default([]) }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Clean old backups on remote host
|
||||
ansible.builtin.find:
|
||||
paths: "{{ app_backup_path_remote | default(app_dir + '/backups') }}"
|
||||
patterns: "{{ app_name }}-*.tar.gz"
|
||||
age: "{{ app_backup_retention_days_remote | default(7) }}d"
|
||||
register: old_backups_remote
|
||||
|
||||
- name: Remove old backups from remote host
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ old_backups_remote.files | default([]) }}"
|
||||
|
||||
always:
|
||||
- name: Restart application after backup
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: present
|
||||
when: app_backup_stop_services | default(true)
|
||||
|
||||
tags: [backup]
|
||||
when: app_backup | bool
|
||||
39
tasks/deploy.yaml
Normal file
39
tasks/deploy.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
- name: "{{ app_name }} - Deploy Docker Compose configuration"
|
||||
block:
|
||||
- name: Copy Docker Compose template
|
||||
ansible.builtin.template:
|
||||
src: "{{ app_compose_template }}"
|
||||
dest: "{{ app_compose_dest }}"
|
||||
mode: "{{ app_permission_mode }}"
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
validate: "{{ 'docker compose -f %s config -q' if app_compose_validate else omit }}"
|
||||
when: _compose_type == 'template'
|
||||
register: compose_file
|
||||
|
||||
- name: Copy Docker Compose file
|
||||
ansible.builtin.copy:
|
||||
src: "{{ app_compose_file }}"
|
||||
dest: "{{ app_compose_dest }}"
|
||||
mode: "{{ app_permission_mode }}"
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
validate: "{{ 'docker compose -f %s config -q' if app_compose_validate else omit }}"
|
||||
when: _compose_type == 'file'
|
||||
register: compose_file
|
||||
|
||||
- name: Copy Docker Compose content from YAML string
|
||||
ansible.builtin.copy:
|
||||
content: "{{ app_compose_content }}"
|
||||
dest: "{{ app_compose_dest }}"
|
||||
mode: "{{ app_permission_mode }}"
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
validate: "{{ 'docker compose -f %s config -q' if app_compose_validate else omit }}"
|
||||
when: _compose_type == 'content'
|
||||
register: compose_file
|
||||
|
||||
- ansible.builtin.include_tasks: manage_compose.yaml
|
||||
when: app_compose_start | default(true)
|
||||
tags: [deploy]
|
||||
49
tasks/health_check.yaml
Normal file
49
tasks/health_check.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
- name: Wait for containers to be healthy
|
||||
community.docker.docker_container_info:
|
||||
name: "{{ app_name }}"
|
||||
register: container_info
|
||||
until: container_info.container.State.Health.Status | default('healthy') == 'healthy'
|
||||
retries: "{{ app_health_check_retries | default(30) }}"
|
||||
delay: "{{ app_health_check_delay | default(10) }}"
|
||||
when: app_health_check | default(true) and app_health_check_method | default('docker') == 'docker'
|
||||
ignore_errors: true
|
||||
|
||||
- name: Verify application HTTP endpoint
|
||||
ansible.builtin.uri:
|
||||
url: "{{ app_health_check_url }}"
|
||||
method: GET
|
||||
status_code: "{{ app_health_check_status_codes | default([200, 201, 202]) }}"
|
||||
register: http_health_check
|
||||
until: http_health_check.status in (app_health_check_status_codes | default([200, 201, 202]))
|
||||
retries: "{{ app_health_check_retries | default(30) }}"
|
||||
delay: "{{ app_health_check_delay | default(10) }}"
|
||||
when: app_health_check | default(true) and app_health_check_method | default('docker') == 'http' and app_health_check_url is defined
|
||||
ignore_errors: true
|
||||
|
||||
- name: Check if all containers are running
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose ps --services --filter status=running
|
||||
chdir: "{{ app_dir }}"
|
||||
register: running_services
|
||||
when: app_health_check | default(true)
|
||||
|
||||
- name: Check total services
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose ps --services
|
||||
chdir: "{{ app_dir }}"
|
||||
register: total_services
|
||||
when: app_health_check | default(true)
|
||||
|
||||
- name: Verify deployment success
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- running_services.stdout_lines | length == total_services.stdout_lines | length
|
||||
fail_msg: "Not all containers are running. Running: {{ running_services.stdout_lines | length }}, Total: {{ total_services.stdout_lines | length }}"
|
||||
success_msg: "All containers are running successfully."
|
||||
when: app_health_check | default(true) and total_services.stdout_lines is defined
|
||||
|
||||
- name: Log deployment status
|
||||
ansible.builtin.debug:
|
||||
msg: "Deployment {{ 'successful' if (running_services.stdout_lines | length == total_services.stdout_lines | length) else 'failed' }}"
|
||||
when: app_health_check | default(true) and total_services.stdout_lines is defined
|
||||
14
tasks/main.yaml
Normal file
14
tasks/main.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
|
||||
- ansible.builtin.include_tasks: setup.yaml
|
||||
tags: [setup, deploy]
|
||||
- ansible.builtin.include_tasks: restore.yaml
|
||||
tags: [restore, never]
|
||||
- ansible.builtin.include_tasks: deploy.yaml
|
||||
tags: [deploy]
|
||||
- ansible.builtin.include_tasks: health_check.yaml
|
||||
tags: [deploy, healthcheck]
|
||||
- ansible.builtin.include_tasks: backup.yaml
|
||||
tags: [backup, never]
|
||||
- ansible.builtin.include_tasks: update.yaml
|
||||
tags: [update, never]
|
||||
14
tasks/manage_compose.yaml
Normal file
14
tasks/manage_compose.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: "{{ app_name }} - Stop Docker Compose stack"
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: stopped
|
||||
remove_orphans: true
|
||||
ignore_errors: true
|
||||
|
||||
- name: "{{ app_name }} - Start Docker Compose stack"
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: present
|
||||
pull: "{{ app_compose_pull | default('policy') }}"
|
||||
recreate: "{{ app_compose_recreate | default('auto') }}"
|
||||
120
tasks/restore.yaml
Normal file
120
tasks/restore.yaml
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
- name: "{{ app_name }} - Restore application data"
|
||||
block:
|
||||
- name: Set specific archive if provided
|
||||
ansible.builtin.set_fact:
|
||||
_restore_backup: "{{ app_restore_archive }}"
|
||||
when: app_restore_archive is defined
|
||||
|
||||
- name: Verify specific archive exists (controller)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ _restore_backup }}"
|
||||
delegate_to: localhost
|
||||
register: specific_archive_stat
|
||||
when: app_restore_archive is defined and app_restore_source | default('controller') == 'controller'
|
||||
|
||||
- name: Verify specific archive exists (remote)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ _restore_backup }}"
|
||||
register: specific_archive_stat
|
||||
when: app_restore_archive is defined and app_restore_source | default('controller') == 'remote'
|
||||
|
||||
- name: Fail if specific archive doesn't exist
|
||||
ansible.builtin.fail:
|
||||
msg: "Specified restore archive does not exist: {{ _restore_backup }}"
|
||||
when: app_restore_archive is defined and not specific_archive_stat.stat.exists
|
||||
|
||||
- name: Find latest backup on controller
|
||||
ansible.builtin.find:
|
||||
paths: "{{ app_backup_path }}"
|
||||
patterns: "{{ app_name }}-*.tar.gz"
|
||||
age: "-{{ app_restore_max_age_days | default(30) }}d"
|
||||
delegate_to: localhost
|
||||
register: controller_backups
|
||||
when: app_restore_source | default('controller') == 'controller' and app_restore_archive is not defined
|
||||
|
||||
- name: Find latest backup on remote host
|
||||
ansible.builtin.find:
|
||||
paths: "{{ app_backup_path_remote | default(app_dir + '/backups') }}"
|
||||
patterns: "{{ app_name }}-*.tar.gz"
|
||||
age: "-{{ app_restore_max_age_days | default(30) }}d"
|
||||
register: remote_backups
|
||||
when: app_restore_source | default('controller') == 'remote' and app_restore_archive is not defined
|
||||
|
||||
- name: Set latest backup file (controller)
|
||||
ansible.builtin.set_fact:
|
||||
_restore_backup: "{{ (controller_backups.files | sort(attribute='mtime') | last).path }}"
|
||||
when: app_restore_source | default('controller') == 'controller' and controller_backups.files | length > 0 and app_restore_archive is not defined
|
||||
|
||||
- name: Set latest backup file (remote)
|
||||
ansible.builtin.set_fact:
|
||||
_restore_backup: "{{ (remote_backups.files | sort(attribute='mtime') | last).path }}"
|
||||
when: app_restore_source | default('controller') == 'remote' and remote_backups.files | length > 0 and app_restore_archive is not defined
|
||||
|
||||
- name: Copy backup from controller to remote if needed
|
||||
ansible.builtin.copy:
|
||||
src: "{{ _restore_backup }}"
|
||||
dest: "{{ app_dir }}/restore_backup.tar.gz"
|
||||
owner: "{{ app_uid | default(omit) }}"
|
||||
group: "{{ app_gid | default(omit) }}"
|
||||
mode: "{{ app_permission_mode | default('0644') }}"
|
||||
when: app_restore_source | default('controller') == 'controller' and _restore_backup is defined
|
||||
register: copied_backup
|
||||
|
||||
- name: Set restore path for remote backup
|
||||
ansible.builtin.set_fact:
|
||||
_restore_path: "{{ _restore_backup if app_restore_source | default('controller') == 'remote' else copied_backup.dest }}"
|
||||
when: _restore_backup is defined
|
||||
|
||||
- name: Stop application for restore
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: stopped
|
||||
when: _restore_path is defined
|
||||
|
||||
- name: Debug directory permissions before deletion
|
||||
ansible.builtin.shell: |
|
||||
ls -la "{{ item }}"
|
||||
ls -la "{{ item | dirname }}"
|
||||
whoami
|
||||
id
|
||||
loop: "{{ app_backup_subdirectories | default([]) }}"
|
||||
when: _restore_path is defined
|
||||
become: true
|
||||
register: debug_perms
|
||||
|
||||
- name: Show debug output
|
||||
ansible.builtin.debug:
|
||||
var: debug_perms
|
||||
|
||||
- name: Remove existing backup subdirectories for clean restore
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop: "{{ app_backup_subdirectories | default([]) }}"
|
||||
when: _restore_path is defined
|
||||
become: true
|
||||
|
||||
- name: Extract backup to application directory
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ _restore_path }}"
|
||||
dest: "{{ app_dir }}"
|
||||
owner: "{{ app_uid | default(omit) }}"
|
||||
group: "{{ app_gid | default(omit) }}"
|
||||
remote_src: true
|
||||
when: _restore_path is defined
|
||||
|
||||
- name: Clean up copied backup file
|
||||
ansible.builtin.file:
|
||||
path: "{{ app_dir }}/restore_backup.tar.gz"
|
||||
state: absent
|
||||
when: app_restore_source | default('controller') == 'controller' and copied_backup is defined
|
||||
|
||||
always:
|
||||
- name: Start application after restore
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: present
|
||||
when: _restore_path is defined
|
||||
|
||||
tags: [restore]
|
||||
111
tasks/setup.yaml
Normal file
111
tasks/setup.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
- name: "{{ app_name }} - Validate deployment requirements"
|
||||
block:
|
||||
- name: Verify Docker is available
|
||||
ansible.builtin.command: docker --version
|
||||
register: docker_check
|
||||
failed_when: docker_check.rc != 0
|
||||
changed_when: false
|
||||
|
||||
- name: Verify Docker Compose is available
|
||||
ansible.builtin.command: docker compose version
|
||||
register: compose_check
|
||||
failed_when: compose_check.rc != 0
|
||||
changed_when: false
|
||||
|
||||
- name: Validate required variables
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- app_name is defined
|
||||
- app_name | length > 0
|
||||
- app_name is match('^[a-zA-Z0-9_-]+$')
|
||||
- host_root_path is defined
|
||||
- host_root_path | length > 0
|
||||
- host_root_path is match('^/[a-zA-Z0-9/_-]*$')
|
||||
fail_msg: "Validation failed for {{ app_name | default('undefined') }}: Required variables missing or contain invalid characters. app_name and host_root_path must be defined and contain only alphanumeric, underscore, hyphen, and slash characters."
|
||||
|
||||
- name: Set compose source type
|
||||
ansible.builtin.set_fact:
|
||||
_compose_type: >-
|
||||
{{
|
||||
'template' if app_compose_template is not none else
|
||||
'file' if app_compose_file is not none else
|
||||
'content' if app_compose_content is not none else
|
||||
'none'
|
||||
}}
|
||||
|
||||
- name: Validate compose source
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _compose_type != 'none'
|
||||
fail_msg: "No Docker Compose source specified for {{ app_name }}: Must define one of app_compose_template, app_compose_file, or app_compose_content"
|
||||
|
||||
- name: Validate directory paths
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- app_dir is match('^/[a-zA-Z0-9/_-]*$')
|
||||
- not (app_dir is match('.*\.\..*'))
|
||||
fail_msg: "Security validation failed for {{ app_name }}: app_dir '{{ app_dir }}' contains invalid characters or path traversal sequences"
|
||||
|
||||
- name: Check if compose template exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ app_compose_template }}"
|
||||
register: _compose_template_stat
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
when: app_compose_template is defined and app_compose_template | length > 0
|
||||
|
||||
- name: Assert compose template exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _compose_template_stat.stat.exists
|
||||
fail_msg: "Template file not found for {{ app_name }}: '{{ app_compose_template }}' does not exist"
|
||||
when: app_compose_template is defined and app_compose_template | length > 0
|
||||
|
||||
- name: Check if compose file exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ app_compose_file }}"
|
||||
register: _compose_file_stat
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
when: app_compose_file is defined and app_compose_file
|
||||
|
||||
- name: Assert compose file exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _compose_file_stat.stat.exists
|
||||
fail_msg: "Docker Compose file not found for {{ app_name }}: '{{ app_compose_file }}' does not exist"
|
||||
when: app_compose_file is defined and app_compose_file
|
||||
tags: [setup, deploy]
|
||||
|
||||
- name: "{{ app_name }} - Create application directories"
|
||||
block:
|
||||
- name: Create application directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ app_dir }}"
|
||||
state: directory
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
mode: "{{ app_permission_mode }}"
|
||||
|
||||
- name: Create application subdirectories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
mode: "{{ app_permission_mode }}"
|
||||
loop: "{{ app_subdirectories }}"
|
||||
tags: [setup, deploy]
|
||||
|
||||
- name: "{{ app_name }} - Deploy additional templates"
|
||||
block:
|
||||
- name: Copy over additional templates
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
mode: "{{ app_permission_mode }}"
|
||||
loop: "{{ app_extra_templates }}"
|
||||
tags: [setup, deploy]
|
||||
31
tasks/update.yaml
Normal file
31
tasks/update.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
- name: "{{ app_name }} - Update application"
|
||||
block:
|
||||
- name: Stop application for update
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: stopped
|
||||
|
||||
- name: Pull latest container images
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
pull: always
|
||||
|
||||
- name: Start application with new images
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: present
|
||||
recreate: always
|
||||
register: update_restart
|
||||
|
||||
- name: Verify application health after update
|
||||
ansible.builtin.include_tasks: health_check.yaml
|
||||
when: app_health_check | default(true)
|
||||
|
||||
rescue:
|
||||
- name: Update failed, attempt to restart application
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_dir }}"
|
||||
state: present
|
||||
|
||||
tags: [update]
|
||||
Reference in New Issue
Block a user