D5.4 Registry ฯƒฯ„ฮฟ Workflow ๐Ÿ”„

ฮคฮฟ ฯ€ฮปฮฎฯฮตฯ‚ CI/CD Workflow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Developer                                              โ”‚
โ”‚      โ”‚ git push                                         โ”‚
โ”‚      โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  CI/CD Pipeline (GitHub Actions / Jenkins)      โ”‚    โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ 1. Checkout code                           โ”‚    โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ 2. Run tests                               โ”‚    โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ 3. Build Docker image                      โ”‚    โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ 4. Push โ†’ Private Registry                 โ”‚    โ”‚
โ”‚  โ”‚  โ””โ”€โ”€ 5. Trigger Ansible deploy                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                          โ”‚                              โ”‚
โ”‚                          โ–ผ                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Ansible Controller (bananapim4berry)           โ”‚    โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ Pull image ฮฑฯ€ฯŒ registry                    โ”‚    โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ Deploy ฯƒฮต servers                          โ”‚    โ”‚
โ”‚  โ”‚  โ””โ”€โ”€ Verify deployment                          โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                          โ”‚                              โ”‚
โ”‚              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                  โ”‚
โ”‚              โ–ผ                       โ–ผ                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
โ”‚  โ”‚   server1       โ”‚     โ”‚   server2       โ”‚            โ”‚
โ”‚  โ”‚   myapp:2.0.0   โ”‚     โ”‚   myapp:2.0.0   โ”‚            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ฮ’ฮฎฮผฮฑ 1: Build & Push Role

mkdir -p ~/ansible/roles/app_publish/tasks

cat > ~/ansible/roles/app_publish/tasks/main.yml << 'EOF'
---
# ============================================================
# App Publish Role
# Build โ†’ Tag โ†’ Push โ†’ Private Registry
# ============================================================

# โ”€โ”€ Pre-checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Verify app version defined
  ansible.builtin.assert:
    that:
      - app_version is defined
      - app_version | length > 0
    fail_msg: "โŒ app_version is required!"

- name: Verify build directory exists
  ansible.builtin.stat:
    path: "{{ app_build_dir }}"
  register: build_dir_stat

- name: Assert build directory exists
  ansible.builtin.assert:
    that:
      - build_dir_stat.stat.exists
      - build_dir_stat.stat.isdir
    fail_msg: "โŒ Build directory not found: {{ app_build_dir }}"

# โ”€โ”€ Build โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Build Docker image
  community.docker.docker_image:
    name:   "{{ app_name }}"
    tag:    "{{ app_version }}"
    source: build
    build:
      path:    "{{ app_build_dir }}"
      pull:    true
      nocache: "{{ docker_build_nocache | default(false) }}"
      args:
        APP_VERSION: "{{ app_version }}"
        BUILD_DATE:  "{{ ansible_facts['date_time']['date'] }}"
        GIT_COMMIT:  "{{ git_commit | default('unknown') }}"
  register: build_result

- name: Build result
  ansible.builtin.debug:
    msg: "โœ… Built: {{ app_name }}:{{ app_version }} ({{ build_result.image.Id[:12] }})"

# โ”€โ”€ Tag โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Tag image ฮณฮนฮฑ registry
  community.docker.docker_image:
    name:       "{{ app_name }}:{{ app_version }}"
    repository: "{{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}"
    tag:        "{{ item }}"
    source:     local
  loop:
    - "{{ app_version }}"
    - latest
  loop_control:
    label: "{{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}:{{ item }}"

# โ”€โ”€ Login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Login ฯƒฯ„ฮฟ private registry
  community.docker.docker_login:
    registry_url: "https://{{ registry_hostname }}:{{ registry_ext_port }}"
    username:     "{{ registry_deploy_user }}"
    password:     "{{ vault_registry_user_pass }}"
    tls_verify:   false
  no_log: true
  register: reg_login

# โ”€โ”€ Push โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Push image ฯƒฯ„ฮฟ registry
  community.docker.docker_image:
    name:       "{{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}"
    tag:        "{{ item }}"
    source:     local
    push:       true
    tls_verify: false
  loop:
    - "{{ app_version }}"
    - latest
  loop_control:
    label: "{{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}:{{ item }}"
  register: push_result

- name: Push result
  ansible.builtin.debug:
    msg: "โœ… Pushed: {{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}:{{ item }}"
  loop:
    - "{{ app_version }}"
    - latest

# โ”€โ”€ Logout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Logout ฮฑฯ€ฯŒ registry
  community.docker.docker_login:
    registry_url: "https://{{ registry_hostname }}:{{ registry_ext_port }}"
    username:     "{{ registry_deploy_user }}"
    state:        absent
  no_log: true

# โ”€โ”€ Cleanup local build image โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Remove local build image
  community.docker.docker_image:
    name:         "{{ app_name }}"
    tag:          "{{ app_version }}"
    state:        absent
    force_absent: true
  when: docker_cleanup_after_push | default(true)
EOF

ฮ’ฮฎฮผฮฑ 2: Pull & Deploy Role

mkdir -p ~/ansible/roles/app_deploy/tasks

cat > ~/ansible/roles/app_deploy/tasks/main.yml << 'EOF'
---
# ============================================================
# App Deploy Role
# Pull from Registry โ†’ Deploy โ†’ Verify
# ============================================================

# โ”€โ”€ Pre-deploy backup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Get current container version
  community.docker.docker_container_info:
    name: "{{ app_name }}"
  register: current_container
  ignore_errors: true

- name: Save current version
  ansible.builtin.set_fact:
    previous_image: >-
      {{ current_container.container.Config.Image
         if current_container.exists
         else 'none' }}
  when: current_container is not failed

- name: Show current version
  ansible.builtin.debug:
    msg: "Current: {{ previous_image | default('none') }}"

# โ”€โ”€ Trust registry certificate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Ensure registry cert directory exists
  ansible.builtin.file:
    path:  "/etc/docker/certs.d/{{ registry_hostname }}:{{ registry_ext_port }}"
    state: directory
    mode:  '0755'

- name: Fetch registry certificate
  ansible.builtin.command:
    cmd: >
      openssl s_client
      -connect {{ registry_hostname }}:{{ registry_ext_port }}
      -showcerts </dev/null 2>/dev/null
      | openssl x509 -outform PEM
  register:     registry_cert
  changed_when: false
  ignore_errors: true

- name: Install registry certificate
  ansible.builtin.copy:
    content: "{{ registry_cert.stdout }}"
    dest:    "/etc/docker/certs.d/{{ registry_hostname }}:{{ registry_ext_port }}/ca.crt"
    mode:    '0644'
  when: not registry_cert.failed

# โ”€โ”€ Login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Login ฯƒฯ„ฮฟ registry
  community.docker.docker_login:
    registry_url: "https://{{ registry_hostname }}:{{ registry_ext_port }}"
    username:     "{{ registry_deploy_user }}"
    password:     "{{ vault_registry_user_pass }}"
    tls_verify:   false
  no_log: true

# โ”€โ”€ Pull new image โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Pull new image
  community.docker.docker_image:
    name:         "{{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}"
    tag:          "{{ app_version }}"
    source:       pull
    force_source: true
    tls_verify:   false
  register: pull_result

- name: Pull result
  ansible.builtin.debug:
    msg: "โœ… Pulled: {{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}:{{ app_version }}"

# โ”€โ”€ Logout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Logout ฮฑฯ€ฯŒ registry
  community.docker.docker_login:
    registry_url: "https://{{ registry_hostname }}:{{ registry_ext_port }}"
    username:     "{{ registry_deploy_user }}"
    state:        absent
  no_log: true

# โ”€โ”€ Stop old container โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Stop old container
  community.docker.docker_container:
    name:  "{{ app_name }}"
    state: stopped
  when:
    - current_container is not failed
    - current_container.exists
  ignore_errors: true

# โ”€โ”€ Deploy new container โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Deploy new container
  community.docker.docker_container:
    name:           "{{ app_name }}"
    image:          "{{ registry_hostname }}:{{ registry_ext_port }}/{{ app_name }}:{{ app_version }}"
    state:          started
    restart_policy: unless-stopped
    recreate:       true
    ports:          "{{ app_ports | default([]) }}"
    env:            "{{ app_env_vars | default({}) }}"
    volumes:        "{{ app_volumes | default([]) }}"
    networks:       "{{ app_networks | default([]) }}"
    memory:         "{{ app_memory  | default('256m') }}"
    cpus:           "{{ app_cpus    | default('0.5') }}"
    labels:
      app:         "{{ app_name }}"
      version:     "{{ app_version }}"
      deployed-at: "{{ ansible_facts['date_time']['iso8601'] }}"
      deployed-by: ansible
    log_driver: json-file
    log_options:
      max-size: "10m"
      max-file: "3"
    healthcheck:
      test:         "{{ app_healthcheck | default(['CMD-SHELL', 'exit 0']) }}"
      interval:     30s
      timeout:      10s
      retries:      3
      start_period: 30s
  no_log: "{{ app_env_vars is defined and app_env_vars | length > 0 }}"
  register: deploy_result

# โ”€โ”€ Wait for healthy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Wait for container to be healthy
  community.docker.docker_container_info:
    name: "{{ app_name }}"
  register: health_check
  until: >
    health_check.container is defined and
    health_check.container.State.Running == true and
    (health_check.container.State.Health is not defined or
     health_check.container.State.Health.Status in ['healthy', 'starting'])
  retries: 12
  delay:   5

# โ”€โ”€ Verify HTTP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: HTTP health check
  ansible.builtin.uri:
    url:         "{{ app_health_url | default('http://localhost/health') }}"
    status_code: 200
    timeout:     30
  register:      http_check
  retries:       5
  delay:         5
  ignore_errors: true

# โ”€โ”€ Rollback ฮฑฮฝ failed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Rollback ฮฑฮฝ deploy failed
  community.docker.docker_container:
    name:     "{{ app_name }}"
    image:    "{{ previous_image }}"
    state:    started
    recreate: true
  when:
    - http_check.failed
    - previous_image is defined
    - previous_image != 'none'
  register: rollback_result

- name: Rollback notification
  ansible.builtin.debug:
    msg: "โš ๏ธ ROLLBACK to {{ previous_image }}!"
  when:
    - http_check.failed
    - rollback_result is not skipped

# โ”€โ”€ Cleanup old images โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Remove dangling images
  community.docker.docker_prune:
    images:         true
    images_filters:
      dangling: true
  when: not http_check.failed

# โ”€โ”€ Deploy report โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Deploy report
  ansible.builtin.debug:
    msg:
      - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
      - "{{ 'โœ… DEPLOY OK' if not http_check.failed else 'โŒ DEPLOY FAILED โ€” ROLLED BACK' }}"
      - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
      - "App    : {{ app_name }}"
      - "Version: {{ app_version }}"
      - "Host   : {{ inventory_hostname }}"
      - "Status : {{ health_check.container.State.Status }}"
      - "HTTP   : {{ 'โœ… OK' if not http_check.failed else 'โŒ Failed' }}"
      - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
EOF

ฮ’ฮฎฮผฮฑ 3: ฮ ฮปฮฎฯฮตฯ‚ Workflow Playbook

cat > ~/ansible/playbooks/registry-workflow.yml << 'EOF'
---
# ============================================================
# Full Registry Workflow
# Build โ†’ Push โ†’ Deploy
# ฮงฯฮฎฯƒฮท: ansible-playbook registry-workflow.yml
#         -e "workflow=build_push" -e "app_version=2.0.0"
#     ฮฎ: -e "workflow=deploy"     -e "app_version=2.0.0"
#     ฮฎ: -e "workflow=full"       -e "app_version=2.0.0"
# ============================================================

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# PLAY 1: BUILD & PUSH (ฯƒฯ„ฮฟฮฝ controller)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
- name: Build & Push Application Image
  hosts: "{{ build_host | default('localhost') }}"
  become: false
  gather_facts: true

  vars:
    workflow:            "{{ workflow | default('full') }}"
    app_name:            myapp
    app_version:         "{{ app_version | default('1.0.0') }}"
    app_build_dir:       "/opt/{{ app_name }}/src"
    registry_hostname:   nextcloud
    registry_ext_port:   443
    registry_deploy_user: deploy

  tasks:

    - name: Build & Push info
      ansible.builtin.debug:
        msg:
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
          - "Phase: BUILD & PUSH"
          - "App  : {{ app_name }}:{{ app_version }}"
          - "Registry: {{ registry_hostname }}:{{ registry_ext_port }}"
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
      tags: always

    - name: Build & Push image
      ansible.builtin.import_role:
        name: app_publish
      when: workflow in ['build_push', 'full']
      tags: [build, push]

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# PLAY 2: DEPLOY (ฯƒฯ„ฮฟฯ…ฯ‚ servers)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
- name: Deploy Application
  hosts: "{{ target | default('all_managed') }}"
  become: true
  gather_facts: true
  serial: "{{ deploy_serial | default(1) }}"    # โ† rolling update!

  vars:
    workflow:            "{{ workflow | default('full') }}"
    app_name:            myapp
    app_version:         "{{ app_version | default('1.0.0') }}"
    registry_hostname:   nextcloud
    registry_ext_port:   443
    registry_deploy_user: deploy

    # โ”€โ”€ App configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    app_ports:
      - "8080:8080"

    app_env_vars:
      NODE_ENV:    production
      APP_VERSION: "{{ app_version }}"
      PORT:        "8080"

    app_volumes:
      - "{{ app_name }}_data:/app/data"

    app_networks:
      - name: app_network

    app_memory: "256m"
    app_cpus:   "0.5"

    app_healthcheck:
      - CMD-SHELL
      - "wget -q -O /dev/null http://localhost:8080/health || exit 1"

    app_health_url: "http://localhost:8080/health"

  pre_tasks:

    - name: Deploy info
      ansible.builtin.debug:
        msg:
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
          - "Phase: DEPLOY"
          - "Host : {{ inventory_hostname }}"
          - "App  : {{ app_name }}:{{ app_version }}"
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
      tags: always

    - name: Create app network
      community.docker.docker_network:
        name:  app_network
        state: present
      tags: setup

    - name: Create app volume
      community.docker.docker_volume:
        name:  "{{ app_name }}_data"
        state: present
      tags: setup

  tasks:

    - name: Deploy application
      ansible.builtin.import_role:
        name: app_deploy
      when: workflow in ['deploy', 'full']
      tags: deploy

  post_tasks:

    - name: Final status
      ansible.builtin.command:
        cmd: "docker ps --filter name={{ app_name }} --format '{{ '{{' }}.Names{{ '}}' }}\t{{ '{{' }}.Status{{ '}}' }}\t{{ '{{' }}.Image{{ '}}' }}'"
      register:     final_status
      changed_when: false
      tags: always

    - name: Show final status
      ansible.builtin.debug:
        msg: "{{ final_status.stdout_lines }}"
      tags: always

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# PLAY 3: VERIFY ALL
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
- name: Verify All Deployments
  hosts: "{{ target | default('all_managed') }}"
  become: true
  gather_facts: false

  vars:
    app_name:    myapp
    app_version: "{{ app_version | default('1.0.0') }}"

  tasks:

    - name: Verify all servers
      ansible.builtin.uri:
        url:         "http://localhost:8080/health"
        status_code: 200
      register:      verify_check
      ignore_errors: true
      tags: verify

    - name: Final verification report
      ansible.builtin.debug:
        msg:
          - "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"
          - "โ•‘   DEPLOYMENT VERIFICATION            โ•‘"
          - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
          - "โ•‘ Host   : {{ inventory_hostname }}"
          - "โ•‘ App    : {{ app_name }}:{{ app_version }}"
          - "โ•‘ Status : {{ 'โœ… OK' if not verify_check.failed else 'โŒ FAILED' }}"
          - "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"
      tags: always
EOF

ฮ’ฮฎฮผฮฑ 4: GitHub Actions Integration

# .github/workflows/deploy.yml
---
name: Build & Deploy

on:
  push:
    branches: [main]
    tags:     ['v*']

jobs:
  build-push-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install Ansible
        run: |
          pip install ansible
          ansible-galaxy collection install \
              community.docker

      # โ”€โ”€ Extract version โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      - name: Get version
        id: version
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
          else
            echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
          fi

      # โ”€โ”€ Setup SSH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -p 2022 ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

      # โ”€โ”€ Setup Vault โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      - name: Setup Vault password
        run: |
          echo "${{ secrets.VAULT_PASSWORD }}" > ~/.vault_pass
          chmod 600 ~/.vault_pass

      # โ”€โ”€ Create ansible.cfg โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      - name: Create ansible.cfg
        run: |
          cat > ansible.cfg << 'EOF'
          [defaults]
          vault_password_file = ~/.vault_pass
          host_key_checking = False
          remote_user = ansible
          private_key_file = ~/.ssh/id_ed25519
          EOF

      # โ”€โ”€ Create inventory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      - name: Create inventory
        run: |
          cat > inventory.ini << EOF
          [all_managed]
          nextcloud ansible_host=${{ secrets.SERVER_IP }} ansible_port=2022
          EOF

      # โ”€โ”€ Run workflow โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      - name: Build Push Deploy
        run: |
          ansible-playbook playbooks/registry-workflow.yml \
              -i inventory.ini \
              -e "workflow=full" \
              -e "app_version=${{ steps.version.outputs.version }}" \
              -e "vault_registry_user_pass=${{ secrets.REGISTRY_PASS }}" \
              -v

Registry Catalog Management

# tasks/registry_manage.yml
---
# ============================================================
# Registry Management Tasks
# ============================================================

# โ”€โ”€ List all images โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Get registry catalog
  ansible.builtin.uri:
    url:              "https://{{ registry_hostname }}:{{ registry_ext_port }}/v2/_catalog"
    validate_certs:   false
    url_username:     "{{ registry_deploy_user }}"
    url_password:     "{{ vault_registry_user_pass }}"
    force_basic_auth: true
  register: catalog
  no_log:   true

- name: Show catalog
  ansible.builtin.debug:
    msg: "Repositories: {{ catalog.json.repositories }}"

# โ”€โ”€ Get image tags โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Get tags ฮณฮนฮฑ image
  ansible.builtin.uri:
    url:              "https://{{ registry_hostname }}:{{ registry_ext_port }}/v2/{{ app_name }}/tags/list"
    validate_certs:   false
    url_username:     "{{ registry_deploy_user }}"
    url_password:     "{{ vault_registry_user_pass }}"
    force_basic_auth: true
  register: image_tags
  no_log:   true

- name: Show image tags
  ansible.builtin.debug:
    msg: "{{ app_name }} tags: {{ image_tags.json.tags | default([]) }}"

# โ”€โ”€ Cleanup old tags โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Keep only last N versions
  ansible.builtin.debug:
    msg: >
      Tags to keep:
      {{ image_tags.json.tags | default([]) | sort | last(keep_versions | default(5)) }}

# โ”€โ”€ Registry disk usage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Check registry disk usage
  ansible.builtin.command:
    cmd: "du -sh {{ registry_data_dir }}"
  register:     disk_usage
  changed_when: false

- name: Disk usage
  ansible.builtin.debug:
    msg: "Registry storage: {{ disk_usage.stdout }}"

ฮ•ฮบฯ„ฮญฮปฮตฯƒฮท Workflow

# โ”€โ”€ Full workflow (build + push + deploy) โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/registry-workflow.yml \
    --limit nextcloud \
    -e "workflow=full" \
    -e "app_version=2.0.0" \
    -v

# โ”€โ”€ ฮœฯŒฮฝฮฟ build & push โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/registry-workflow.yml \
    -e "workflow=build_push" \
    -e "app_version=2.0.0" \
    --tags "build,push"

# โ”€โ”€ ฮœฯŒฮฝฮฟ deploy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/registry-workflow.yml \
    --limit nextcloud \
    -e "workflow=deploy" \
    -e "app_version=2.0.0" \
    --tags deploy

# โ”€โ”€ Rolling update (serial=1) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/registry-workflow.yml \
    -e "workflow=full" \
    -e "app_version=2.0.0" \
    -e "deploy_serial=1"

# โ”€โ”€ Dry run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/registry-workflow.yml \
    --limit nextcloud \
    -e "workflow=deploy" \
    -e "app_version=2.0.0" \
    --check --diff