Compare commits

...
9 Commits
12 changed files with 175 additions and 75 deletions
+29 -17
View File
@@ -209,7 +209,7 @@ ansible-playbook --tags update site.yml
**Update process:** **Update process:**
1. Stops the application 1. Stops the application
2. Pulls latest images for all services 2. Pulls images according to `app_compose_pull` (defaults to `policy`; set to `always` to force fresh pulls)
3. Recreates containers with new images 3. Recreates containers with new images
4. Runs health checks (if enabled) 4. Runs health checks (if enabled)
@@ -290,13 +290,23 @@ The file will be copied as-is to the application directory without Jinja2 proces
## Configuration Reference ## Configuration Reference
### Required Externals
These variables have no defaults and must be set by the calling playbook (typically as group_vars or host_vars). They are consumed by the defaults shown further down; if you override every derived variable that uses them, you can leave the corresponding external unset.
| Variable | Description | Consumed by |
|----------|-------------|-------------|
| `host_root_path` | Base directory on the **target host** under which each app's directory is created (e.g. `/srv/apps`, `/opt/docker`) | `app_dir` |
| `backup_path` | Base directory on the **controller** where fetched backup archives are stored (e.g. `~/backups`) | `app_backup_path` |
| `app_roles_path` | Absolute path to the roles directory that contains your per-app shim roles; used so this role can locate the shim role's `templates/` directory (e.g. `{{ playbook_dir }}/roles`) | `app_templates_path` |
| `app_role_name` | Name of the per-app shim role that depends on this one (e.g. `my-app`, `jellyfin`); combined with `app_roles_path` to find its `templates/` directory | `app_templates_path` |
### Core Variables ### Core Variables
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `app_name` | Required | Unique application instance name | | `app_name` | Required | Unique application instance name |
| `role_name` | Required | Role name for template paths | | `app_dir` | `{{ host_root_path }}/{{ app_name }}` | Application directory on the target host |
| `app_dir` | `{{ host_root_path }}/{{ app_name }}` | Application directory |
| `app_uid` | `{{ ansible_facts.user_uid }}` | File ownership UID | | `app_uid` | `{{ ansible_facts.user_uid }}` | File ownership UID |
| `app_gid` | `{{ ansible_facts.user_gid }}` | File ownership GID | | `app_gid` | `{{ ansible_facts.user_gid }}` | File ownership GID |
| `app_permission_mode` | `"0640"` | File permission mode | | `app_permission_mode` | `"0640"` | File permission mode |
@@ -309,8 +319,9 @@ The file will be copied as-is to the application directory without Jinja2 proces
| `app_compose_file` | - | Static compose file path | | `app_compose_file` | - | Static compose file path |
| `app_compose_content` | - | Inline compose content | | `app_compose_content` | - | Inline compose content |
| `app_compose_validate` | `true` | Validate compose syntax | | `app_compose_validate` | `true` | Validate compose syntax |
| `app_compose_pull` | `policy` | Image pull strategy | | `app_compose_pull` | `policy` | Image pull strategy (also used by `update` — set to `always` to force pulls on update) |
| `app_compose_recreate` | `auto` | Container recreate strategy | | `app_compose_recreate` | `auto` | Container recreate strategy |
| `app_compose_project_name` | `{{ app_dir \| basename }}` | Compose project name; override when your compose file sets a non-default project name |
### Backup Configuration ### Backup Configuration
@@ -335,6 +346,7 @@ The file will be copied as-is to the application directory without Jinja2 proces
|----------|---------|-------------| |----------|---------|-------------|
| `app_health_check` | `true` | Enable health checks | | `app_health_check` | `true` | Enable health checks |
| `app_health_check_method` | `docker` | Check method (`docker`/`http`) | | `app_health_check_method` | `docker` | Check method (`docker`/`http`) |
| `app_container_name` | `{{ app_name }}` | Container name inspected by the `docker` health check; override when your compose service uses a different `container_name` |
| `app_health_check_url` | - | HTTP endpoint for health check | | `app_health_check_url` | - | HTTP endpoint for health check |
| `app_health_check_retries` | `30` | Number of check retries | | `app_health_check_retries` | `30` | Number of check retries |
| `app_health_check_delay` | `10` | Delay between checks (seconds) | | `app_health_check_delay` | `10` | Delay between checks (seconds) |
@@ -369,44 +381,44 @@ app_extra_templates:
The role is organized into logical task files for selective execution: The role is organized into logical task files for selective execution:
``` ```
├── setup.yaml # Validation, directories, and templates ├── setup.yml # Validation, directories, and templates
├── restore.yaml # Backup restoration logic ├── restore.yml # Backup restoration logic
├── deploy.yaml # Docker Compose deployment and startup ├── deploy.yml # Docker Compose deployment and startup
├── health_check.yaml # Health monitoring and verification ├── health_check.yml # Health monitoring and verification
├── backup.yaml # Backup creation and management ├── backup.yml # Backup creation and management
├── update.yaml # Container update and image pulling ├── update.yml # Container update and image pulling
└── manage_compose.yaml # Docker Compose lifecycle management └── manage_compose.yml # Docker Compose lifecycle management
``` ```
### Task Execution Flow ### Task Execution Flow
1. **Setup** (`setup.yaml`) - [setup, deploy] 1. **Setup** (`setup.yml`) - [setup, deploy]
- Docker/Compose validation - Docker/Compose validation
- Variable validation and security checks - Variable validation and security checks
- Directory creation - Directory creation
- Additional template deployment - Additional template deployment
2. **Restore** (`restore.yaml`) - [restore, never] 2. **Restore** (`restore.yml`) - [restore, never]
- Backup file selection and validation - Backup file selection and validation
- Service stopping - Service stopping
- Archive extraction - Archive extraction
- Service restart - Service restart
3. **Deploy** (`deploy.yaml`) - [deploy] 3. **Deploy** (`deploy.yml`) - [deploy]
- Docker Compose file creation - Docker Compose file creation
- Container startup and management - Container startup and management
4. **Health Check** (`health_check.yaml`) - [deploy, healthcheck] 4. **Health Check** (`health_check.yml`) - [deploy, healthcheck]
- Service health verification - Service health verification
- Endpoint monitoring - Endpoint monitoring
5. **Backup** (`backup.yaml`) - [backup, never] 5. **Backup** (`backup.yml`) - [backup, never]
- Service stopping (optional) - Service stopping (optional)
- Archive creation - Archive creation
- Backup copying and cleanup - Backup copying and cleanup
- Service restart - Service restart
6. **Update** (`update.yaml`) - [update, never] 6. **Update** (`update.yml`) - [update, never]
- Service stopping - Service stopping
- Image pulling - Image pulling
- Container recreation - Container recreation
+106
View File
@@ -0,0 +1,106 @@
roles_path=../roles
# Convert string to valid Ansible variable name
convert_to_ansible_var() {
local input="$1"
# Replace hyphens, and spaces with underscores
local result=$(echo "$input" | tr '-' '_' | tr ' ' '_')
# Remove any characters that aren't alphanumeric or underscore
result=$(echo "$result" | tr -cd '[:alnum:]_')
# Ensure it doesn't start with a number (prepend underscore if it does)
if [[ "$result" =~ ^[0-9] ]]; then
result="_${result}"
fi
# Convert to lowercase (Ansible convention)
result=$(echo "$result" | tr '[:upper:]' '[:lower:]')
echo "$result"
}
# Check if any arguments were provided
if [ $# -eq 0 ]; then
echo "No arguments provided."
echo "Usage: $0 role1 role2 ..."
exit 1
fi
pushd $roles_path
for arg in "$@"; do
echo "Creating directory $arg"
mkdir $arg
cd $arg
echo "Creating subdirectories"
mkdir {defaults,templates,meta}
echo "Creating main.yml files"
touch {defaults,meta}/main.yml
touch templates/compose.yml.j2
app_name=$(convert_to_ansible_var "$arg")
# create files
cat > defaults/main.yml << EOF
---
app_name: $arg
# Container configuration
${app_name}_container_name: "{{ app_name | default('$arg') }}"
${app_name}_container_version: latest
${app_name}_restart_policy: "{{ app_restart_policy }}"
# Network configuration
# ${app_name}_http_port: 8080
# Volume paths
# ${app_name}_config_path: "{{ app_dir }}/config"
# ${app_name}_data_path: "{{ app_dir }}/data"
# App-specific configuration
# Add custom variables here
# Directory structure
# app_subdirectories:
# - config
# - data
# Backup configuration
# app_backup_subdirectories:
# - config
EOF
cat > meta/main.yml << EOF
---
galaxy_info:
author: <your_name>
description: Deploy $arg with Docker Compose
license: MIT
dependencies:
- role: docker_compose_app
vars:
app_role_name: $arg
EOF
cat > templates/compose.yml.j2 << EOF
---
services:
${arg}:
image: "your-image:{{ ${app_name}_container_version }}"
container_name: "{{ ${app_name}_container_name }}"
restart: "{{ ${app_name}_restart_policy }}"
# ports:
# - "{{ ${app_name}_http_port }}:8080"
# volumes:
# - "{{ ${app_name}_config_path }}:/config"
# - "{{ ${app_name}_data_path }}:/data"
EOF
cd ..
done
popd
+2
View File
@@ -21,7 +21,9 @@ app_compose_validate: true
app_compose_pull: policy app_compose_pull: policy
app_compose_recreate: auto app_compose_recreate: auto
app_compose_start: true app_compose_start: true
app_compose_project_name: "{{ app_dir | basename }}"
app_container_name: "{{ app_name }}"
app_health_check: true app_health_check: true
app_health_check_method: docker app_health_check_method: docker
app_health_check_retries: 30 app_health_check_retries: 30
+1 -1
View File
@@ -34,6 +34,6 @@
when: _compose_type == 'content' when: _compose_type == 'content'
register: compose_file register: compose_file
- ansible.builtin.include_tasks: manage_compose.yaml - ansible.builtin.include_tasks: manage_compose.yml
when: app_compose_start | default(true) when: app_compose_start | default(true)
tags: [deploy] tags: [deploy]
@@ -1,7 +1,7 @@
--- ---
- name: Wait for containers to be healthy - name: Wait for container to be healthy
community.docker.docker_container_info: community.docker.docker_container_info:
name: "{{ app_name }}" name: "{{ app_container_name }}"
register: container_info register: container_info
until: container_info.container.State.Health.Status | default('healthy') == 'healthy' until: container_info.container.State.Health.Status | default('healthy') == 'healthy'
retries: "{{ app_health_check_retries | default(30) }}" retries: "{{ app_health_check_retries | default(30) }}"
@@ -21,29 +21,23 @@
when: app_health_check | default(true) and app_health_check_method | default('docker') == 'http' and app_health_check_url is defined when: app_health_check | default(true) and app_health_check_method | default('docker') == 'http' and app_health_check_url is defined
ignore_errors: true ignore_errors: true
- name: Check if all containers are running - name: Inspect compose project containers
ansible.builtin.command: community.docker.docker_host_info:
cmd: docker compose ps --services --filter status=running containers: true
chdir: "{{ app_dir }}" containers_all: true
register: running_services containers_filters:
when: app_health_check | default(true) label: "com.docker.compose.project={{ app_compose_project_name }}"
register: compose_containers
- name: Check total services
ansible.builtin.command:
cmd: docker compose ps --services
chdir: "{{ app_dir }}"
register: total_services
when: app_health_check | default(true) when: app_health_check | default(true)
- name: Verify deployment success - name: Verify deployment success
vars:
_total: "{{ compose_containers.containers | length }}"
_running: "{{ compose_containers.containers | selectattr('State', 'equalto', 'running') | list | length }}"
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- running_services.stdout_lines | length == total_services.stdout_lines | length - compose_containers.containers | length > 0
fail_msg: "Not all containers are running. Running: {{ running_services.stdout_lines | length }}, Total: {{ total_services.stdout_lines | length }}" - _running == _total
fail_msg: "Not all containers are running. Running: {{ _running }}, Total: {{ _total }}"
success_msg: "All containers are running successfully." success_msg: "All containers are running successfully."
when: app_health_check | default(true) and total_services.stdout_lines is defined when: app_health_check | default(true) and compose_containers.containers 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
View File
@@ -1,14 +0,0 @@
---
- 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
View File
@@ -0,0 +1,14 @@
---
- ansible.builtin.include_tasks: setup.yml
tags: [setup, deploy]
- ansible.builtin.include_tasks: restore.yml
tags: [restore, never]
- ansible.builtin.include_tasks: deploy.yml
tags: [deploy]
- ansible.builtin.include_tasks: health_check.yml
tags: [deploy, healthcheck]
- ansible.builtin.include_tasks: backup.yml
tags: [backup, never]
- ansible.builtin.include_tasks: update.yml
tags: [update, never]
-15
View File
@@ -72,21 +72,6 @@
state: stopped state: stopped
when: _restore_path is defined 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 - name: Remove existing backup subdirectories for clean restore
ansible.builtin.file: ansible.builtin.file:
path: "{{ item }}" path: "{{ item }}"
+5 -4
View File
@@ -18,11 +18,12 @@
that: that:
- app_name is defined - app_name is defined
- app_name | length > 0 - app_name | length > 0
- app_name is match('^[a-zA-Z0-9_-]+$') - app_name is match('^[a-zA-Z0-9_.-]+$')
- host_root_path is defined - host_root_path is defined
- host_root_path | length > 0 - host_root_path | length > 0
- host_root_path is match('^/[a-zA-Z0-9/_-]*$') - 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." - not (host_root_path is match('.*\.\..*'))
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, contain only alphanumeric, dot, underscore, hyphen, and slash characters, and must not contain path traversal sequences."
- name: Set compose source type - name: Set compose source type
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -43,7 +44,7 @@
- name: Validate directory paths - name: Validate directory paths
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- app_dir is match('^/[a-zA-Z0-9/_-]*$') - app_dir is match('^/[a-zA-Z0-9./_-]*$')
- not (app_dir is match('.*\.\..*')) - not (app_dir is match('.*\.\..*'))
fail_msg: "Security validation failed for {{ app_name }}: app_dir '{{ app_dir }}' contains invalid characters or path traversal sequences" fail_msg: "Security validation failed for {{ app_name }}: app_dir '{{ app_dir }}' contains invalid characters or path traversal sequences"
+2 -2
View File
@@ -9,7 +9,7 @@
- name: Pull latest container images - name: Pull latest container images
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ app_dir }}" project_src: "{{ app_dir }}"
pull: always pull: "{{ app_compose_pull | default('always') }}"
- name: Start application with new images - name: Start application with new images
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
@@ -19,7 +19,7 @@
register: update_restart register: update_restart
- name: Verify application health after update - name: Verify application health after update
ansible.builtin.include_tasks: health_check.yaml ansible.builtin.include_tasks: health_check.yml
when: app_health_check | default(true) when: app_health_check | default(true)
rescue: rescue: