--- - name: Container Update Orchestrator hosts: all gather_facts: yes vars: update_timestamp: "{{ ansible_date_time.iso8601 }}" update_report_dir: "/tmp/update_reports" rollback_enabled: true update_timeout: 600 health_check_retries: 5 health_check_delay: 10 tasks: - name: Create update reports directory file: path: "{{ update_report_dir }}" state: directory mode: '0755' delegate_to: localhost run_once: true - name: Check if Docker is available shell: command -v docker >/dev/null 2>&1 register: docker_available changed_when: false ignore_errors: yes - name: Skip Docker tasks if not available set_fact: skip_docker: "{{ docker_available.rc != 0 }}" - name: Pre-update system check shell: | echo "=== PRE-UPDATE SYSTEM CHECK ===" # System resources echo "System Resources:" echo "Memory: $(free -h | awk 'NR==2{print $3"/"$2" ("$3*100/$2"%)"}')" echo "Disk: $(df -h / | awk 'NR==2{print $3"/"$2" ("$5")"}')" echo "Load: $(uptime | awk -F'load average:' '{print $2}')" echo "" # Docker status if command -v docker >/dev/null 2>&1; then echo "Docker Status:" echo "Running containers: $(docker ps -q 2>/dev/null | wc -l)" echo "Total containers: $(docker ps -aq 2>/dev/null | wc -l)" echo "Images: $(docker images -q 2>/dev/null | wc -l)" echo "Docker daemon: $(docker info >/dev/null 2>&1 && echo 'OK' || echo 'ERROR')" else echo "Docker not available" fi echo "" # Network connectivity echo "Network Connectivity:" ping -c 1 8.8.8.8 >/dev/null 2>&1 && echo "Internet: OK" || echo "Internet: FAILED" # Tailscale connectivity if command -v tailscale >/dev/null 2>&1; then tailscale status >/dev/null 2>&1 && echo "Tailscale: OK" || echo "Tailscale: FAILED" fi register: pre_update_check changed_when: false - name: Discover updatable containers shell: | if ! command -v docker >/dev/null 2>&1; then echo "Docker not available" exit 0 fi echo "=== CONTAINER UPDATE DISCOVERY ===" # Get current container information echo "Current Container Status:" docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}" 2>/dev/null echo "" # Check for available image updates echo "Checking for image updates:" docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | grep -v "" | while read image; do if [ -n "$image" ]; then echo "Checking: $image" # Pull latest image to compare if docker pull "$image" >/dev/null 2>&1; then # Compare image IDs current_id=$(docker images "$image" --format "{{.ID}}" | head -1) echo " Current ID: $current_id" # Check if any containers are using this image containers_using=$(docker ps --filter "ancestor=$image" --format "{{.Names}}" 2>/dev/null | tr '\n' ' ') if [ -n "$containers_using" ]; then echo " Used by containers: $containers_using" else echo " No running containers using this image" fi else echo " ❌ Failed to pull latest image" fi echo "" fi done register: container_discovery changed_when: false when: not skip_docker - name: Create container backup snapshots shell: | if ! command -v docker >/dev/null 2>&1; then echo "Docker not available" exit 0 fi echo "=== CREATING CONTAINER SNAPSHOTS ===" # Create snapshots of running containers docker ps --format "{{.Names}}" 2>/dev/null | while read container; do if [ -n "$container" ]; then echo "Creating snapshot for: $container" # Commit container to backup image backup_image="${container}_backup_$(date +%Y%m%d_%H%M%S)" if docker commit "$container" "$backup_image" >/dev/null 2>&1; then echo " ✅ Snapshot created: $backup_image" else echo " ❌ Failed to create snapshot" fi fi done echo "" # Export Docker Compose configurations echo "Backing up Docker Compose files:" find /opt /home -name "docker-compose*.yml" -o -name "compose*.yml" 2>/dev/null | while read compose_file; do if [ -f "$compose_file" ]; then backup_file="/tmp/$(basename "$compose_file").backup.$(date +%Y%m%d_%H%M%S)" cp "$compose_file" "$backup_file" 2>/dev/null && echo " ✅ Backed up: $compose_file -> $backup_file" fi done register: backup_snapshots changed_when: false when: not skip_docker and rollback_enabled - name: Orchestrated container updates block: - name: Update containers by priority groups shell: | echo "=== ORCHESTRATED CONTAINER UPDATES ===" # Define update priority groups # Priority 1: Infrastructure services (databases, caches) # Priority 2: Application services # Priority 3: Monitoring and auxiliary services priority_1="postgres mysql mariadb redis mongo elasticsearch rabbitmq" priority_2="nginx apache traefik caddy" priority_3="grafana prometheus node-exporter" update_group() { local group_name="$1" local containers="$2" echo "Updating $group_name containers..." for pattern in $containers; do matching_containers=$(docker ps --format "{{.Names}}" 2>/dev/null | grep -i "$pattern" || true) for container in $matching_containers; do if [ -n "$container" ]; then echo " Updating: $container" # Get current image current_image=$(docker inspect "$container" --format '{{.Config.Image}}' 2>/dev/null) # Pull latest image if docker pull "$current_image" >/dev/null 2>&1; then echo " ✅ Image updated: $current_image" # Recreate container with new image if docker-compose -f "$(find /opt /home -name "*compose*.yml" -exec grep -l "$container" {} \; | head -1)" up -d "$container" >/dev/null 2>&1; then echo " ✅ Container recreated successfully" # Wait for container to be healthy sleep {{ health_check_delay }} # Check container health if [ "$(docker inspect "$container" --format '{{.State.Status}}' 2>/dev/null)" = "running" ]; then echo " ✅ Container is running" else echo " ❌ Container failed to start" fi else echo " ❌ Failed to recreate container" fi else echo " ⚠️ No image update available" fi echo "" fi done done } # Execute updates by priority update_group "Priority 1 (Infrastructure)" "$priority_1" sleep 30 # Wait between priority groups update_group "Priority 2 (Applications)" "$priority_2" sleep 30 update_group "Priority 3 (Monitoring)" "$priority_3" echo "Orchestrated updates completed" register: orchestrated_updates when: update_mode is defined and update_mode == "orchestrated" - name: Update specific container shell: | echo "=== UPDATING SPECIFIC CONTAINER ===" container="{{ target_container }}" if ! docker ps --format "{{.Names}}" | grep -q "^${container}$"; then echo "❌ Container '$container' not found or not running" exit 1 fi echo "Updating container: $container" # Get current image current_image=$(docker inspect "$container" --format '{{.Config.Image}}' 2>/dev/null) echo "Current image: $current_image" # Pull latest image echo "Pulling latest image..." if docker pull "$current_image"; then echo "✅ Image pulled successfully" # Find compose file compose_file=$(find /opt /home -name "*compose*.yml" -exec grep -l "$container" {} \; | head -1) if [ -n "$compose_file" ]; then echo "Using compose file: $compose_file" # Update container using compose if docker-compose -f "$compose_file" up -d "$container"; then echo "✅ Container updated successfully" # Health check echo "Performing health check..." sleep {{ health_check_delay }} retries={{ health_check_retries }} while [ $retries -gt 0 ]; do if [ "$(docker inspect "$container" --format '{{.State.Status}}' 2>/dev/null)" = "running" ]; then echo "✅ Container is healthy" break else echo "⏳ Waiting for container to be ready... ($retries retries left)" sleep {{ health_check_delay }} retries=$((retries - 1)) fi done if [ $retries -eq 0 ]; then echo "❌ Container failed health check" exit 1 fi else echo "❌ Failed to update container" exit 1 fi else echo "⚠️ No compose file found, using direct Docker commands" docker restart "$container" fi else echo "❌ Failed to pull image" exit 1 fi register: specific_update when: target_container is defined when: not skip_docker - name: Post-update verification shell: | if ! command -v docker >/dev/null 2>&1; then echo "Docker not available" exit 0 fi echo "=== POST-UPDATE VERIFICATION ===" # Check all containers are running echo "Container Status Check:" failed_containers="" docker ps -a --format "{{.Names}}\t{{.Status}}" 2>/dev/null | while IFS=$'\t' read name status; do if [ -n "$name" ]; then if echo "$status" | grep -q "Up"; then echo "✅ $name: $status" else echo "❌ $name: $status" failed_containers="$failed_containers $name" fi fi done # Check system resources after update echo "" echo "System Resources After Update:" echo "Memory: $(free -h | awk 'NR==2{print $3"/"$2" ("$3*100/$2"%)"}')" echo "Load: $(uptime | awk -F'load average:' '{print $2}')" # Check for any error logs echo "" echo "Recent Error Logs:" docker ps --format "{{.Names}}" 2>/dev/null | head -5 | while read container; do if [ -n "$container" ]; then errors=$(docker logs "$container" --since="5m" 2>&1 | grep -i error | wc -l) if [ "$errors" -gt "0" ]; then echo "⚠️ $container: $errors error(s) in last 5 minutes" fi fi done register: post_update_verification changed_when: false when: not skip_docker - name: Rollback on failure shell: | if ! command -v docker >/dev/null 2>&1; then echo "Docker not available" exit 0 fi echo "=== ROLLBACK PROCEDURE ===" # Check if rollback is needed failed_containers=$(docker ps -a --filter "status=exited" --format "{{.Names}}" 2>/dev/null | head -5) if [ -n "$failed_containers" ]; then echo "Failed containers detected: $failed_containers" echo "Initiating rollback..." for container in $failed_containers; do echo "Rolling back: $container" # Find backup image backup_image=$(docker images --format "{{.Repository}}" | grep "${container}_backup_" | head -1) if [ -n "$backup_image" ]; then echo " Found backup image: $backup_image" # Stop current container docker stop "$container" 2>/dev/null || true docker rm "$container" 2>/dev/null || true # Start container from backup image if docker run -d --name "$container" "$backup_image"; then echo " ✅ Rollback successful" else echo " ❌ Rollback failed" fi else echo " ⚠️ No backup image found" fi done else echo "No rollback needed - all containers are healthy" fi register: rollback_result when: not skip_docker and rollback_enabled and (orchestrated_updates.rc is defined and orchestrated_updates.rc != 0) or (specific_update.rc is defined and specific_update.rc != 0) ignore_errors: yes - name: Cleanup old backup images shell: | if ! command -v docker >/dev/null 2>&1; then echo "Docker not available" exit 0 fi echo "=== CLEANUP OLD BACKUPS ===" # Remove backup images older than 7 days old_backups=$(docker images --format "{{.Repository}}\t{{.CreatedAt}}" | grep "_backup_" | awk '$2 < "'$(date -d '7 days ago' '+%Y-%m-%d')'"' | cut -f1) if [ -n "$old_backups" ]; then echo "Removing old backup images:" for backup in $old_backups; do echo " Removing: $backup" docker rmi "$backup" 2>/dev/null || echo " Failed to remove $backup" done else echo "No old backup images to clean up" fi # Clean up temporary backup files find /tmp -name "*.backup.*" -mtime +7 -delete 2>/dev/null || true register: cleanup_result when: not skip_docker ignore_errors: yes - name: Create update report set_fact: update_report: timestamp: "{{ update_timestamp }}" hostname: "{{ inventory_hostname }}" docker_available: "{{ not skip_docker }}" pre_update_check: "{{ pre_update_check.stdout }}" container_discovery: "{{ container_discovery.stdout if not skip_docker else 'Docker not available' }}" backup_snapshots: "{{ backup_snapshots.stdout if not skip_docker and rollback_enabled else 'Snapshots disabled' }}" orchestrated_updates: "{{ orchestrated_updates.stdout if orchestrated_updates is defined else 'Not performed' }}" specific_update: "{{ specific_update.stdout if specific_update is defined else 'Not performed' }}" post_update_verification: "{{ post_update_verification.stdout if not skip_docker else 'Docker not available' }}" rollback_result: "{{ rollback_result.stdout if rollback_result is defined else 'Not needed' }}" cleanup_result: "{{ cleanup_result.stdout if not skip_docker else 'Docker not available' }}" - name: Display update report debug: msg: | ========================================== 🔄 CONTAINER UPDATE REPORT - {{ inventory_hostname }} ========================================== 📊 DOCKER AVAILABLE: {{ 'Yes' if update_report.docker_available else 'No' }} 🔍 PRE-UPDATE CHECK: {{ update_report.pre_update_check }} 🔍 CONTAINER DISCOVERY: {{ update_report.container_discovery }} 💾 BACKUP SNAPSHOTS: {{ update_report.backup_snapshots }} 🔄 ORCHESTRATED UPDATES: {{ update_report.orchestrated_updates }} 🎯 SPECIFIC UPDATE: {{ update_report.specific_update }} ✅ POST-UPDATE VERIFICATION: {{ update_report.post_update_verification }} ↩️ ROLLBACK RESULT: {{ update_report.rollback_result }} 🧹 CLEANUP RESULT: {{ update_report.cleanup_result }} ========================================== - name: Generate JSON update report copy: content: | { "timestamp": "{{ update_report.timestamp }}", "hostname": "{{ update_report.hostname }}", "docker_available": {{ update_report.docker_available | lower }}, "pre_update_check": {{ update_report.pre_update_check | to_json }}, "container_discovery": {{ update_report.container_discovery | to_json }}, "backup_snapshots": {{ update_report.backup_snapshots | to_json }}, "orchestrated_updates": {{ update_report.orchestrated_updates | to_json }}, "specific_update": {{ update_report.specific_update | to_json }}, "post_update_verification": {{ update_report.post_update_verification | to_json }}, "rollback_result": {{ update_report.rollback_result | to_json }}, "cleanup_result": {{ update_report.cleanup_result | to_json }}, "recommendations": [ "Test updates in staging environment first", "Monitor container health after updates", "Maintain regular backup snapshots", "Keep rollback procedures tested and ready", "Schedule updates during maintenance windows" ] } dest: "{{ update_report_dir }}/{{ inventory_hostname }}_container_updates_{{ ansible_date_time.epoch }}.json" delegate_to: localhost - name: Summary message debug: msg: | 🔄 Container update orchestration complete for {{ inventory_hostname }} 📄 Report saved to: {{ update_report_dir }}/{{ inventory_hostname }}_container_updates_{{ ansible_date_time.epoch }}.json {% if target_container is defined %} 🎯 Updated container: {{ target_container }} {% endif %} {% if update_mode is defined %} 🔄 Update mode: {{ update_mode }} {% endif %} 💡 Use -e target_container= to update specific containers 💡 Use -e update_mode=orchestrated for priority-based updates 💡 Use -e rollback_enabled=false to disable automatic rollback