--- - name: Deploy Airflow Workers hosts: airflow_workers gather_facts: yes vars_files: - "{{ inventory_dir }}/group_vars/all/generated_vars.yml" - "{{ inventory_dir }}/group_vars/all/vault.yml" pre_tasks: - name: Announce worker deployment debug: msg: "Starting deployment for Airflow Worker: {{ inventory_hostname }} ({{ ansible_user }}@{{ ansible_host }})" - name: Configure system timezone # Ensures all services and logs on this node use a consistent timezone. community.general.timezone: name: "{{ host_timezone }}" become: yes - name: Install NTP for time synchronization ansible.builtin.apt: name: ntp state: present become: yes - name: Ensure NTP service is started and enabled ansible.builtin.service: name: ntp state: started enabled: yes become: yes - name: Set deploy_group to a valid single group name set_fact: deploy_group: "ytdl" - name: Ensure deploy group exists group: name: "{{ deploy_group }}" state: present become: yes - name: Ensure deploy user exists user: name: "{{ ansible_user }}" group: "{{ deploy_group }}" state: present become: yes - name: Validate deploy_group variable ansible.builtin.assert: that: - deploy_group is defined - deploy_group is string - "',' not in deploy_group" - "' ' not in deploy_group" fail_msg: "The 'deploy_group' variable ('{{ deploy_group }}') must be a single, valid group name. It should not contain commas or spaces." - name: Check for swapfile stat: path: /swapfile register: swap_file become: yes - name: Create 8GB swapfile command: fallocate -l 8G /swapfile when: not swap_file.stat.exists become: yes - name: Set swapfile permissions file: path: /swapfile mode: '0600' when: not swap_file.stat.exists become: yes - name: Make swap command: mkswap /swapfile when: not swap_file.stat.exists become: yes - name: Check current swap status command: swapon --show register: swap_status changed_when: false become: yes - name: Enable swap command: swapon /swapfile when: "'/swapfile' not in swap_status.stdout" become: yes - name: Add swapfile to fstab lineinfile: path: /etc/fstab regexp: '^/swapfile' line: '/swapfile none swap sw 0 0' state: present become: yes - name: Get GID of the deploy group getent: database: group key: "{{ deploy_group }}" register: deploy_group_info become: yes - name: Set deploy_group_gid fact set_fact: deploy_group_gid: "{{ deploy_group_info.ansible_facts.getent_group[deploy_group][1] }}" when: deploy_group_info.ansible_facts.getent_group is defined and deploy_group in deploy_group_info.ansible_facts.getent_group - name: Ensure deploy_group_gid is set to a valid value set_fact: deploy_group_gid: "0" when: deploy_group_gid is not defined or deploy_group_gid == "" - name: Configure system limits copy: src: "configs/etc/sysctl.d/99-system-limits.conf" dest: "/etc/sysctl.d/99-system-limits.conf" owner: root group: root mode: '0644' become: yes register: limits_sysctl_config_copy - name: Apply sysctl settings for system limits command: sysctl --system become: yes when: limits_sysctl_config_copy.changed - name: Create logs directory structure relative to deployment file: path: "./logs/yt-dlp-ops/communication_logs" state: directory mode: '0755' owner: "{{ ansible_user }}" group: "{{ deploy_group }}" become: yes - name: Ensure worker directory exists ansible.builtin.file: path: "{{ airflow_worker_dir }}" state: directory owner: "{{ ansible_user }}" group: "{{ deploy_group }}" mode: '0755' become: yes - name: Ensure runtime data directories exist with correct ownership ansible.builtin.file: path: "{{ airflow_worker_dir }}/{{ item }}" state: directory owner: "{{ ansible_user }}" group: "{{ deploy_group }}" mode: '0775' recurse: yes loop: - "downloadfiles" - "downloadfiles/videos" - "downloadfiles/videos/in-progress" - "downloadfiles/videos/ready" - "inputfiles" - "dumps" become: yes - name: Create .dockerignore on worker to exclude runtime data from build context ansible.builtin.copy: dest: "{{ airflow_worker_dir }}/.dockerignore" content: | # Exclude build artifacts and virtual environments __pycache__/ *.pyc *.pyo .venv/ venv/ # Exclude sensitive information .env .vault_pass # Exclude local development and OS-specific files .DS_Store .idea/ *.swp # Exclude large directories with runtime data that should not be in the image logs/ downloadfiles/ addfiles/ *downloads/ postgres-data/ redis-data/ minio-data/ owner: "{{ ansible_user }}" group: "{{ deploy_group }}" mode: '0644' become: yes - name: Sync python packages to worker for build context ansible.posix.synchronize: src: "../{{ item }}/" dest: "{{ airflow_worker_dir }}/{{ item }}/" rsync_opts: - "--delete" - "--exclude=.DS_Store" - "--exclude=__pycache__" - "--exclude='*.pyc'" recursive: yes perms: yes loop: - "thrift_model" - "pangramia" - "ytops_client" - "yt_ops_services" become: yes become_user: "{{ ansible_user }}" - name: Sync aria2-pro-docker to worker for build context ansible.posix.synchronize: src: "../airflow/aria2-pro-docker/" dest: "{{ airflow_worker_dir }}/aria2-pro-docker/" rsync_opts: - "--delete" recursive: yes perms: yes become: yes become_user: "{{ ansible_user }}" - name: Ensure bin directory exists on worker for build context ansible.builtin.file: path: "{{ airflow_worker_dir }}/bin" state: directory mode: '0755' become: yes become_user: "{{ ansible_user }}" - name: Sync root files and client utilities to worker for build context ansible.posix.synchronize: src: "../{{ item }}" dest: "{{ airflow_worker_dir }}/{{ item }}" perms: yes loop: - "setup.py" - "VERSION" - "bin/ytops-client" become: yes become_user: "{{ ansible_user }}" - name: Ensure Airflow project directory is writable by the container user (UID 50000) ansible.builtin.file: path: "{{ airflow_worker_dir }}" owner: 50000 group: 50000 become: yes - name: Ensure Airflow subdirectories are writable by the container user (UID 50000) ansible.builtin.file: path: "{{ item }}" owner: 50000 group: 50000 recurse: yes state: directory loop: - "{{ airflow_worker_dir }}/dags" - "{{ airflow_worker_dir }}/logs" - "{{ airflow_worker_dir }}/plugins" - "{{ airflow_worker_dir }}/config" become: yes tasks: - name: Install pipx ansible.builtin.apt: name: pipx state: present become: yes - name: Install Glances for system monitoring ansible.builtin.command: pipx install glances[all] args: creates: "{{ ansible_env.HOME }}/.local/bin/glances" become: yes become_user: "{{ ansible_user }}" - name: Install base system packages for tools ansible.builtin.apt: name: - unzip - wget - xz-utils state: present update_cache: yes become: yes - name: Install required Python packages ansible.builtin.pip: name: - python-dotenv - aria2p - tabulate - redis - PyYAML - aiothrift - PySocks state: present extra_args: --break-system-packages become: yes - name: Install pinned Python packages ansible.builtin.pip: name: - brotli==1.1.0 - certifi==2025.10.05 - curl-cffi==0.13.0 - mutagen==1.47.0 - pycryptodomex==3.23.0 - secretstorage==3.4.0 - urllib3==2.5.0 - websockets==15.0.1 state: present extra_args: --break-system-packages become: yes - name: Upgrade yt-dlp and bgutil provider ansible.builtin.shell: | set -e python3 -m pip install -U --pre "yt-dlp[default,curl-cffi]" --break-system-packages python3 -m pip install --no-cache-dir -U bgutil-ytdlp-pot-provider --break-system-packages args: warn: false become: yes changed_when: true - name: Check for FFmpeg stat: path: /usr/local/bin/ffmpeg register: ffmpeg_binary become: yes - name: Install FFmpeg when: not ffmpeg_binary.stat.exists become: yes block: - name: Create ffmpeg directory ansible.builtin.file: path: /opt/ffmpeg state: directory mode: '0755' - name: Download and unarchive FFmpeg ansible.builtin.unarchive: src: "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz" dest: /opt/ffmpeg remote_src: yes extra_opts: [--strip-components=1] - name: Symlink ffmpeg and ffprobe ansible.builtin.file: src: "/opt/ffmpeg/bin/{{ item }}" dest: "/usr/local/bin/{{ item }}" state: link force: yes loop: - ffmpeg - ffprobe - name: Check for Deno stat: path: /usr/local/bin/deno register: deno_binary become: yes - name: Install Deno when: not deno_binary.stat.exists become: yes block: - name: Download and unarchive Deno ansible.builtin.unarchive: src: https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip dest: /usr/local/bin/ remote_src: yes mode: '0755' - name: Check if ytops_client requirements.txt exists stat: path: "{{ airflow_worker_dir }}/ytops_client/requirements.txt" register: ytops_client_reqs become: yes become_user: "{{ ansible_user }}" - name: Install dependencies from ytops_client/requirements.txt ansible.builtin.pip: requirements: "{{ airflow_worker_dir }}/ytops_client/requirements.txt" state: present extra_args: --break-system-packages when: ytops_client_reqs.stat.exists become: yes # Include Docker health check - name: Include Docker health check tasks include_tasks: tasks/docker_health_check.yml - name: Pull pre-built Docker images for ytdlp-ops services ansible.builtin.command: > docker compose --project-directory . -f configs/docker-compose-ytdlp-ops.yaml pull --ignore-buildable args: chdir: "{{ airflow_worker_dir }}" become: yes become_user: "{{ ansible_user }}" register: docker_pull_result retries: 3 delay: 10 changed_when: "'Pulling' in docker_pull_result.stdout or 'Downloaded' in docker_pull_result.stdout" - name: Show docker pull output ansible.builtin.debug: var: docker_pull_result.stdout_lines when: docker_pull_result.changed roles: - ytdlp-worker