Compare commits
9
Commits
4211fad10c
...
1aff50b74e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aff50b74e | ||
|
|
7f9758a924 | ||
|
|
05c933e2e0 | ||
|
|
eba541a2e2 | ||
|
|
8208f3f779 | ||
|
|
af2fe22bfa | ||
|
|
757ba52377 | ||
|
|
8da028ef5a | ||
|
|
463c783c6f |
@@ -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
|
||||||
|
|||||||
Executable
+106
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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]
|
|
||||||
@@ -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]
|
||||||
@@ -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 }}"
|
||||||
@@ -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"
|
||||||
|
|
||||||
@@ -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:
|
||||||
Reference in New Issue
Block a user