--- # ============================================================================ # ENHANCED DOCKER DEPLOYMENT WITH AUTO-SCALING SUPPORT # ============================================================================ # ============================================================================ # SYSTEM VALIDATION AND REQUIREMENTS CHECK # ============================================================================ - name: Validate deployment environment debug: msg: - "=== AUTO-SCALING DEPLOYMENT VALIDATION ===" - "Target OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" - "Architecture: {{ ansible_architecture }}" - "Available memory: {{ ansible_memtotal_mb }}MB" - "Available disk: {{ ansible_mounts[0].size_available / 1024 / 1024 / 1024 | round(2) }}GB" - "Target replicas: {{ docker_replicas | default(replicas | default(1)) }}" - "Deployment strategy: {{ docker_deployment_strategy | default(deployment_strategy | default('rolling')) }}" - name: Check minimum system requirements for auto-scaling assert: that: - ansible_memtotal_mb >= (docker_replicas | default(1) | int * 512) - (ansible_mounts[0].size_available / 1024 / 1024 / 1024) >= 5 - docker_replicas | default(1) | int <= docker_max_replicas | default(9) | int fail_msg: | Insufficient system resources for {{ docker_replicas | default(1) }} replicas. Need at least {{ docker_replicas | default(1) | int * 512 }}MB RAM and 5GB disk space. Replicas ({{ docker_replicas | default(1) }}) cannot exceed max_replicas ({{ docker_max_replicas | default(9) }}). success_msg: "✅ System requirements met for auto-scaling deployment." # ============================================================================ # PROJECT-SPECIFIC CONFIGURATION LOADING # ============================================================================ - name: Load project-specific vars if exists block: - name: Check if project-specific variables file exists stat: path: "projects/{{ app_name }}/vars.yml" register: project_vars_file - name: Load project-specific variables include_vars: file: "projects/{{ app_name }}/vars.yml" when: project_vars_file.stat.exists - name: Check if project-specific variables by environment file exists stat: path: "projects/{{ app_name }}/group_vars/{{ deploy_env }}.yml" register: project_vars_env_file - name: Load project-specific variables by environment include_vars: file: "projects/{{ app_name }}/group_vars/{{ deploy_env }}.yml" when: project_vars_env_file.stat.exists delegate_to: localhost run_once: true - name: Load port registry include_vars: file: "{{ port_registry_file | default('group_vars/port_registry.yml') }}" name: port_registry delegate_to: localhost run_once: true when: port_registry_file is defined # ============================================================================ # DEPLOYMENT CONFIGURATION SETUP # ============================================================================ - name: Set deployment environment tag set_fact: deploy_env_tag: "{{ deploy_env }}{{ env_tag | default('') }}" - name: Set auto-scaling configuration set_fact: # Auto-scaling parameters final_replicas: "{{ docker_replicas | default(replicas | default(1)) }}" final_max_replicas: "{{ docker_max_replicas | default(max_replicas | default(9)) }}" final_deployment_strategy: "{{ docker_deployment_strategy | default(deployment_strategy | default('rolling')) }}" final_health_check_timeout: "{{ docker_health_check_timeout | default(health_check_timeout | default(30)) }}" final_deployment_timeout: "{{ docker_deployment_timeout | default(deployment_timeout | default(300)) }}" # Port configuration final_app_port: "{{ port_registry.projects[app_name][deploy_env_tag] | default(3000) }}" final_container_port: "{{ container_port | default(3000) }}" # Resource configuration final_memory_limit: "{{ container_memory_limit | default(resource_limits[deploy_env].memory | default('1g')) }}" final_cpu_limit: "{{ container_cpu_limit | default(resource_limits[deploy_env].cpu | default('1.0')) }}" # Network configuration final_docker_network: "{{ docker_network_name | default(app_name + '-' + deploy_env + '-network') }}" final_restart_policy: "{{ container_restart_policy | default('unless-stopped') }}" - name: Display auto-scaling deployment configuration debug: msg: - "🚀 AUTO-SCALING DEPLOYMENT CONFIGURATION" - "========================================" - "Application: {{ app_name }}" - "Environment: {{ deploy_env_tag }}" - "Framework: {{ framework | default('unknown') }}" - "Docker Image: {{ docker_image_name }}:{{ docker_image_tag }}" - "Target Replicas: {{ final_replicas }}" - "Max Replicas: {{ final_max_replicas }}" - "Deployment Strategy: {{ final_deployment_strategy }}" - "Base Port: {{ final_app_port }} (Pattern: {{ final_app_port }}{{ replica_number | default(1) }})" - "Container Port: {{ final_container_port }}" - "Memory Limit: {{ final_memory_limit }}" - "CPU Limit: {{ final_cpu_limit }}" - "Health Check Timeout: {{ final_health_check_timeout }}s" - "Deployment Timeout: {{ final_deployment_timeout }}s" # ============================================================================ # PRE-DEPLOYMENT VALIDATION # ============================================================================ - name: Validate auto-scaling deployment parameters assert: that: - app_name is defined and app_name != "" - deploy_env in ['development', 'staging', 'production'] - docker_image_tag is defined and docker_image_tag != "" - final_replicas | int >= 1 - final_replicas | int <= final_max_replicas | int - final_deployment_strategy in ['rolling', 'blue-green', 'recreate'] - docker_registry is defined fail_msg: "Missing or invalid auto-scaling deployment parameters" success_msg: "✅ Auto-scaling deployment parameters validated" tags: [validate] # ============================================================================ # DIRECTORY AND ENVIRONMENT SETUP # ============================================================================ - name: Create deployment directories file: path: "{{ item }}" state: directory owner: "{{ server_user }}" group: "{{ server_user }}" mode: '0755' loop: - "/home/{{ server_user }}/scripts" - "/home/{{ server_user }}/ci" - "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}" - "/home/{{ server_user }}/ci/data/{{ app_name }}/{{ deploy_env }}" - "/home/{{ server_user }}/ci/logs/{{ app_name }}/{{ deploy_env }}" - "/home/{{ server_user }}/ci/backups/{{ app_name }}/{{ deploy_env }}" - "/home/{{ server_user }}/ci/deployments/{{ app_name }}/{{ deploy_env }}" - name: Create environment file from vault if secrets_dict is defined copy: dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" content: | {% for key, value in secrets_dict.items() %} {{ key }}={{ value }} {% endfor %} when: secrets_dict is defined - name: Load project-specific env if exists block: - name: Check if project-specific .env template exists by environment stat: path: "projects/{{ app_name }}/templates/.env.{{ deploy_env }}.j2" register: custom_env_template_by_env delegate_to: localhost run_once: true - name: Create environment file from project template by environment template: src: "projects/{{ app_name }}/templates/.env.{{ deploy_env }}.j2" dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" owner: "{{ server_user }}" group: "{{ server_user }}" mode: '0600' backup: yes when: env_file_enabled | default(true) and custom_env_template_by_env.stat.exists - name: Check if project-specific .env template exists stat: path: "projects/{{ app_name }}/templates/.env.j2" register: custom_env_template delegate_to: localhost run_once: true - name: Create environment file from project template template: src: "projects/{{ app_name }}/templates/.env.j2" dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" owner: "{{ server_user }}" group: "{{ server_user }}" mode: '0600' backup: yes when: env_file_enabled | default(true) and custom_env_template.stat.exists and not custom_env_template_by_env.stat.exists - name: Create environment file with default template template: src: .env.j2 dest: "/home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env" owner: "{{ server_user }}" group: "{{ server_user }}" mode: '0600' backup: yes when: not custom_env_template_by_env.stat.exists and not custom_env_template.stat.exists vars: # Pass auto-scaling configuration to template auto_scaling_config: replicas: "{{ final_replicas }}" max_replicas: "{{ final_max_replicas }}" deployment_strategy: "{{ final_deployment_strategy }}" base_port: "{{ final_app_port }}" container_port: "{{ final_container_port }}" tags: [environment] when: secrets_dict is not defined # ============================================================================ # DOCKER ENVIRONMENT SETUP # ============================================================================ - name: Setup Docker environment for auto-scaling block: - name: Check if Docker is installed shell: | command -v docker register: docker_check failed_when: false changed_when: false - name: Install Docker (Debian/Ubuntu) apt: name: docker.io state: present update_cache: yes become: yes when: - docker_check.rc != 0 - ansible_os_family == "Debian" - name: Install Docker (CentOS/RHEL) yum: name: docker state: present become: yes when: - docker_check.rc != 0 - ansible_os_family == "RedHat" # Kiểm tra xem Python đã có Docker SDK chưa - name: Check if Docker SDK for Python is installed command: python3 -c "import docker" register: docker_sdk_check failed_when: false changed_when: false # Cài Docker SDK bằng pip nếu chưa có - name: Install Docker SDK for Python via apt (nếu không muốn dùng pip) apt: name: python3-docker state: present become: yes when: - ansible_os_family == "Debian" - docker_sdk_check.rc != 0 - name: Ensure Docker service is started and enabled service: name: docker state: started enabled: true # - name: Create Docker network for auto-scaling deployment # docker_network: # name: "{{ final_docker_network }}" # state: present # driver: bridge # ipam_config: # - subnet: "172.{{ (final_app_port | string)[-2:] | int + 20 }}.0.0/16" - name: Gather Docker networks on host shell: docker network inspect $(docker network ls -q) 2>/dev/null || echo '[]' register: docker_networks_raw changed_when: false failed_when: false - name: Parse Docker networks JSON set_fact: docker_networks_info: "{{ docker_networks_raw.stdout | from_json }}" - name: Extract existing subnets (as strings) from docker networks set_fact: existing_subnets: >- {{ docker_networks_info | default([]) | selectattr('IPAM', 'defined') | selectattr('IPAM.Config', 'defined') | map(attribute='IPAM.Config') | select | map('first') | selectattr('Subnet', 'defined') | map(attribute='Subnet') | list }} - name: Debug existing subnets debug: var: existing_subnets - name: Get used second-octets from existing subnets (172.X.0.0/16 only) set_fact: used_seconds: >- {%- set result = [] -%} {%- for subnet in existing_subnets -%} {%- if subnet.startswith('172.') -%} {%- set parts = subnet.split('.') -%} {%- if parts | length >= 2 -%} {%- set _ = result.append(parts[1] | int) -%} {%- endif -%} {%- endif -%} {%- endfor -%} {{ result }} - name: Debug used second octets debug: var: used_seconds - name: Compute start octet from final_app_port (bounded 20..250) set_fact: start_octet: >- {{ (((final_app_port | string)[-2:] | int) + 20) if (((final_app_port | string)[-2:] | int) + 20) <= 250 else 250 }} - name: Build candidate second-octet list (start -> 250 then 20 -> start-1) set_fact: candidate_list: >- {{ (range(start_octet | int, 251) | list) + (range(20, start_octet | int) | list) }} - name: Debug candidate list debug: msg: "Candidates: {{ candidate_list }}, Used: {{ used_seconds }}" - name: Select first unused second-octet set_fact: chosen_second: >- {{ candidate_list | map('int') | reject('in', used_seconds) | list | first }} - name: Fail if couldn't find free subnet octet fail: msg: "No available 172.X.0.0/16 subnet found in candidate space (20..250). Existing used: {{ used_seconds }}" when: chosen_second is not defined - name: Build chosen subnet set_fact: chosen_subnet: "172.{{ chosen_second }}.0.0/16" - name: Debug chosen subnet debug: msg: "Creating Docker network '{{ final_docker_network }}' with subnet {{ chosen_subnet }}" - name: Create Docker network for auto-scaling deployment community.docker.docker_network: name: "{{ final_docker_network }}" state: present driver: bridge ipam_config: - subnet: "{{ chosen_subnet }}" become: true - name: Pull Docker image for auto-scaling deployment docker_image: name: "{{ docker_image_full | default(docker_image_name + ':' + docker_image_tag) }}" source: pull force_source: true register: docker_pull_result tags: [docker] # ============================================================================ # ENHANCED DOCKER MANAGEMENT SCRIPT GENERATION # ============================================================================ - name: Generate enhanced Docker management script with auto-scaling template: src: manage-docker.sh.j2 dest: "/home/{{ server_user }}/scripts/manage-{{ service_name }}.sh" owner: "{{ server_user }}" group: "{{ server_user }}" mode: '0755' backup: yes vars: # Container configuration server_user: "{{ server_user }}" container_name: "{{ service_name }}" framework: "{{ framework }}" # Auto-scaling configuration default_replicas: "{{ final_replicas }}" max_replicas: "{{ final_max_replicas }}" health_check_timeout: "{{ final_health_check_timeout }}" deployment_timeout: "{{ final_deployment_timeout }}" # Port configuration app_port: "{{ final_app_port }}" container_port: "{{ final_container_port }}" # Resource configuration container_restart_policy: "{{ final_restart_policy }}" container_memory_limit: "{{ final_memory_limit }}" container_cpu_limit: "{{ final_cpu_limit }}" # Network configuration docker_network_name: "{{ final_docker_network }}" # ============================================================================ # AUTO-SCALING DEPLOYMENT EXECUTION # ============================================================================ - name: Check current deployment state shell: | if [ -f "/home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh" ]; then sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status 2>/dev/null || echo "no-containers" else echo "no-script" fi become_user: "{{ server_user }}" register: current_deployment_state changed_when: false - name: Display current deployment state debug: msg: "{{ current_deployment_state.stdout_lines }}" when: current_deployment_state.stdout_lines is defined - name: Ensure Docker is installed and user has permissions block: - name: Add user to docker group user: name: "{{ server_user }}" groups: docker append: yes become: yes - name: Reset SSH connection for group changes meta: reset_connection - name: Test Docker access command: docker version register: docker_test failed_when: docker_test.rc != 0 - name: Execute smart deployment with auto-scaling shell: | sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh deploy {{ replicas | default(1) }} {{ docker_image_tag }} rolling async: 600 poll: 10 # Poll every 10 seconds instead of 0 register: deployment_result become_user: "{{ server_user }}" - name: Check deployment result fail: msg: "Deployment failed: {{ deployment_result.stderr | default('Unknown error') }}" when: - deployment_result.rc is defined - deployment_result.rc != 0 # ============================================================================ # POST-DEPLOYMENT VERIFICATION # ============================================================================ - name: Wait for auto-scaling deployment to settle pause: seconds: 5 tags: [health] - name: Verify auto-scaling deployment completion shell: | sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh health become_user: "{{ server_user }}" register: auto_scaling_health_check retries: 5 delay: 15 until: auto_scaling_health_check.rc == 0 tags: [health] - name: Get final auto-scaling deployment status shell: | sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status become_user: "{{ server_user }}" register: final_auto_scaling_status changed_when: false tags: [health] - name: Extract auto-scaling deployment metrics set_fact: auto_scaling_metrics: running_containers: "{{ final_auto_scaling_status.stdout | regex_search('Running Containers: (\\d+)', '\\1') | first | default('0') }}" current_version: "{{ final_auto_scaling_status.stdout | regex_search('Current Version: ([^\\n]+)', '\\1') | first | default('unknown') }}" base_port: "{{ final_auto_scaling_status.stdout | regex_search('Base Port: (\\d+)', '\\1') | first | default('0') }}" # - name: Validate auto-scaling deployment metrics # assert: # that: # - auto_scaling_metrics.running_containers | int == final_replicas | int # - auto_scaling_metrics.current_version == docker_image_tag # fail_msg: | # Auto-scaling deployment validation failed: # Expected: {{ final_replicas }} replicas with version {{ docker_image_tag }} # Actual: {{ auto_scaling_metrics.running_containers }} replicas with version {{ auto_scaling_metrics.current_version }} # success_msg: "✅ Auto-scaling deployment validation passed" # ============================================================================ # LOAD BALANCER INTEGRATION # ============================================================================ - name: Get active ports from script shell: | sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh deploy_active_ports become_user: "{{ server_user }}" register: active_ports_result when: env_tag | default('') != '-worker' - name: Convert ports to clean list set_fact: active_ports_list: "{{ active_ports_result.stdout.split() }}" when: env_tag | default('') != '-worker' and active_ports_result.stdout | length > 0 - name: Generate nginx upstream configuration template: src: nginx-upstream.conf.j2 dest: "/etc/nginx/conf.d/{{ app_name }}-{{ deploy_env }}-upstream.conf" backup: yes vars: upstream_name: "{{ app_name | regex_replace('[^A-Za-z0-9_]', '_') }}_{{ deploy_env }}_backend" base_port: "{{ final_app_port }}" replica_count: "{{ final_replicas }}" load_balancing_method: "{{ nginx_config.load_balancing_method | default('least_conn') }}" max_fails: "{{ nginx_config.max_fails | default(1) }}" fail_timeout: "{{ nginx_config.fail_timeout | default(10) }}" active_ports: "{{ active_ports_list }}" when: - update_load_balancer | default(true) - env_tag | default('') != '-worker' notify: - test nginx configuration - reload nginx - webapplication purge cache tags: [loadbalancer] # ============================================================================ # DEPLOYMENT SUCCESS SUMMARY # ============================================================================ - name: Display auto-scaling deployment success debug: msg: - "🎉 AUTO-SCALING DEPLOYMENT SUCCESS" - "==================================" - "✅ Successfully deployed {{ app_name }} ({{ framework | default('unknown') }})" - "🔢 Replicas: {{ auto_scaling_metrics.running_containers }}/{{ final_replicas }}" - "🏷️ Version: {{ auto_scaling_metrics.current_version }}" - "🚀 Strategy: {{ final_deployment_strategy }}" - "🌐 Base Port: {{ auto_scaling_metrics.base_port }}" - "🏥 Health Status: All containers healthy" - "⚖️ Load Balancer: Updated with {{ final_replicas }} targets" - "" - "🔧 Management Commands:" - " Status: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh status" - " Scale: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh scale " - " Logs: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh logs" - " Health: ./scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh health" tags: [info] # ============================================================================ # CLEANUP AND MAINTENANCE # ============================================================================ - name: Cleanup old Docker images (keep last 5 versions) shell: | # Enhanced cleanup for auto-scaling deployment sudo /home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh clean become_user: "{{ server_user }}" ignore_errors: true when: cleanup_old_images | default(true) tags: [cleanup] # ============================================================================ # DEPLOYMENT RECORD AND AUDIT TRAIL # ============================================================================ - name: Create auto-scaling deployment record copy: content: | # Auto-Scaling Deployment Record timestamp: "{{ ansible_date_time.iso8601 }}" application: "{{ app_name }}" environment: "{{ deploy_env }}{{ env_tag | default('') }}" version: "{{ docker_image_tag }}" replicas: {{ final_replicas }} max_replicas: {{ final_max_replicas }} strategy: "{{ final_deployment_strategy }}" base_port: "{{ final_app_port }}" framework: "{{ framework | default('unknown') }}" node_version: "{{ node_version | default('unknown') }}" status: "success" duration: "{{ auto_scaling_deployment_result.delta | default('unknown') }}" operator: "{{ ansible_user_id }}" git_commit: "{{ git_sha | default('unknown') }}" git_tag: "{{ git_tag | default('unknown') }}" memory_limit: "{{ final_memory_limit }}" cpu_limit: "{{ final_cpu_limit }}" docker_network: "{{ final_docker_network }}" health_check_timeout: "{{ final_health_check_timeout }}" deployment_timeout: "{{ final_deployment_timeout }}" load_balancer_updated: {{ update_load_balancer | default(true) }} management_script: "/home/{{ server_user }}/scripts/manage-{{ app_name }}-{{ deploy_env }}{{ env_tag | default('') }}.sh" dest: "/home/{{ server_user }}/ci/logs/{{ app_name }}/{{ deploy_env }}/deployment-{{ ansible_date_time.epoch }}.yml" owner: "{{ server_user }}" group: "{{ server_user }}" mode: '0644' tags: [logging] - name: Register auto-scaling deployment in system log lineinfile: path: "/var/log/microservices-deployments.log" line: "{{ ansible_date_time.iso8601 }} | {{ app_name }} | {{ deploy_env }} | {{ docker_image_tag }} | {{ git_tag }} | AUTO-SCALE-SUCCESS | {{ final_replicas }}@{{ auto_scaling_metrics.base_port }} | {{ framework | default('unknown') }}" create: true mode: '0644' tags: [logging] - name: Cleanup old backups file shell: | ls -1t /home/{{ server_user }}/ci/configs/{{ app_name }}/{{ deploy_env }}/.env.*~ 2>/dev/null | tail -n +4 | xargs -r rm -- ls -1t /home/{{ server_user }}/scripts/manage-{{ service_name }}.sh.*~ 2>/dev/null | tail -n +4 | xargs -r rm -- ls -1t /etc/nginx/conf.d/{{ app_name }}-{{ deploy_env }}-upstream.conf.*~ 2>/dev/null | tail -n +4 | xargs -r rm -- args: warn: false