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