Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Successful in 30s
Security Vulnerability Scan / Check for Dependency Changes (push) Successful in 23s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Successful in 11s
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
Security Vulnerability Scan / Composer Security Audit (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Successful in 12s
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Failing after 11m15s
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been cancelled
- Replace docker_image module with shell command for more reliable pulling - Add detailed error output from pull command (stdout/stderr) - Show actual docker pull error messages when pull fails - Simplify pull logic - always attempt pull regardless of local existence
439 lines
18 KiB
YAML
439 lines
18 KiB
YAML
---
|
|
- name: Deploy Docker Image to Application Stack
|
|
hosts: "{{ deployment_hosts | default('production') }}"
|
|
gather_facts: yes
|
|
become: no
|
|
|
|
vars:
|
|
# Application code directory (where docker-compose files are located)
|
|
application_code_dest: "/home/deploy/michaelschiemer/current"
|
|
application_compose_suffix: >-
|
|
{%- if deployment_environment == 'staging' -%}
|
|
staging.yml
|
|
{%- else -%}
|
|
production.yml
|
|
{%- endif -%}
|
|
# Image to deploy (can be overridden via -e image_tag=...)
|
|
image_tag: "{{ image_tag | default('latest') }}"
|
|
docker_registry: "{{ docker_registry | default('registry.michaelschiemer.de') }}"
|
|
app_name_default: "framework"
|
|
# Deployment environment (staging or production)
|
|
deployment_environment: "{{ deployment_environment | default('production') }}"
|
|
|
|
tasks:
|
|
- name: Check if vault file exists locally
|
|
stat:
|
|
path: "{{ playbook_dir }}/../secrets/{{ deployment_environment }}.vault.yml"
|
|
delegate_to: localhost
|
|
register: vault_file_stat
|
|
become: no
|
|
|
|
- name: Load secrets from vault file if exists
|
|
include_vars:
|
|
file: "{{ playbook_dir }}/../secrets/{{ deployment_environment }}.vault.yml"
|
|
when: vault_file_stat.stat.exists
|
|
no_log: yes
|
|
ignore_errors: yes
|
|
delegate_to: localhost
|
|
become: no
|
|
|
|
- name: Set app_name from provided value or default
|
|
ansible.builtin.set_fact:
|
|
app_name: "{{ app_name if (app_name is defined and app_name != '') else app_name_default }}"
|
|
|
|
- name: Extract registry URL from docker-compose file (for image deployment)
|
|
ansible.builtin.shell: |
|
|
cd {{ application_code_dest }}
|
|
grep -h "image:" docker-compose.base.yml docker-compose.{{ application_compose_suffix }} 2>/dev/null | \
|
|
grep -E "{{ app_name }}" | head -1 | \
|
|
sed -E 's/.*image:\s*([^\/]+).*/\1/' | \
|
|
sed -E 's/\/.*$//' || echo "localhost:5000"
|
|
register: compose_registry_url
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Set local registry (where containers expect the image)
|
|
ansible.builtin.set_fact:
|
|
local_registry: "{{ (compose_registry_url.stdout | trim) if (compose_registry_url.stdout | trim != '') else 'localhost:5000' }}"
|
|
|
|
- name: Set source registry (where workflow pushes the image)
|
|
ansible.builtin.set_fact:
|
|
source_registry: "{{ docker_registry }}"
|
|
|
|
- name: Set deploy_image from source registry (for pulling)
|
|
ansible.builtin.set_fact:
|
|
deploy_image: "{{ source_registry }}/{{ app_name }}:{{ image_tag }}"
|
|
|
|
- name: Set local_image (where containers expect the image)
|
|
ansible.builtin.set_fact:
|
|
local_image: "{{ local_registry }}/{{ app_name }}:{{ image_tag }}"
|
|
|
|
- name: Set database and MinIO variables from vault or defaults
|
|
ansible.builtin.set_fact:
|
|
db_username: "{{ db_username | default(vault_db_user | default('postgres')) }}"
|
|
db_password: "{{ db_password | default(vault_db_password | default('')) }}"
|
|
minio_root_user: "{{ minio_root_user | default(vault_minio_root_user | default('minioadmin')) }}"
|
|
minio_root_password: "{{ minio_root_password | default(vault_minio_root_password | default('')) }}"
|
|
secrets_dir: "{{ secrets_dir | default('./secrets') }}"
|
|
git_repository_url: "{{ git_repository_url | default(vault_git_repository_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git')) }}"
|
|
git_branch: >-
|
|
{%- if deployment_environment == 'staging' -%}
|
|
staging
|
|
{%- else -%}
|
|
main
|
|
{%- endif -%}
|
|
git_token: "{{ git_token | default(vault_git_token | default('')) }}"
|
|
git_username: "{{ git_username | default(vault_git_username | default('')) }}"
|
|
git_password: "{{ git_password | default(vault_git_password | default('')) }}"
|
|
no_log: yes
|
|
|
|
- name: Determine Docker registry password from vault or extra vars
|
|
ansible.builtin.set_fact:
|
|
registry_password: >-
|
|
{%- if docker_registry_password is defined and docker_registry_password | string | trim != '' -%}
|
|
{{ docker_registry_password }}
|
|
{%- elif vault_docker_registry_password is defined and vault_docker_registry_password | string | trim != '' -%}
|
|
{{ vault_docker_registry_password }}
|
|
{%- else -%}
|
|
{{ '' }}
|
|
{%- endif -%}
|
|
no_log: yes
|
|
|
|
- name: Check if registry is accessible
|
|
ansible.builtin.uri:
|
|
url: "https://{{ docker_registry }}/v2/"
|
|
method: GET
|
|
status_code: [200, 401]
|
|
timeout: 5
|
|
validate_certs: no
|
|
register: registry_check
|
|
ignore_errors: yes
|
|
delegate_to: "{{ inventory_hostname }}"
|
|
become: no
|
|
|
|
- name: Set registry accessible flag
|
|
ansible.builtin.set_fact:
|
|
registry_accessible: "{{ 'true' if (registry_check.status is defined and registry_check.status | int in [200, 401]) else 'false' }}"
|
|
|
|
- name: Login to Docker registry
|
|
community.docker.docker_login:
|
|
registry_url: "{{ docker_registry }}"
|
|
username: "{{ docker_registry_username | default('admin') }}"
|
|
password: "{{ registry_password }}"
|
|
when:
|
|
- registry_password | string | trim != ''
|
|
- registry_accessible == 'true'
|
|
no_log: yes
|
|
ignore_errors: yes
|
|
register: docker_login_result
|
|
|
|
- name: Display image pull information
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Attempting to pull image: {{ deploy_image }}"
|
|
- "Source registry: {{ source_registry }}"
|
|
- "Local registry: {{ local_registry }}"
|
|
- "Registry accessible: {{ registry_accessible | default('unknown') }}"
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
|
|
- name: Check if image already exists locally
|
|
ansible.builtin.shell: |
|
|
docker images --format "{{ '{{' }}.Repository{{ '}}' }}:{{ '{{' }}.Tag{{ '}}' }}" | grep -E "^{{ deploy_image | regex_escape }}$" || echo "NOT_FOUND"
|
|
register: image_exists_before_pull
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Display image existence check
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Image exists before pull: {{ image_exists_before_pull.stdout | default('unknown') }}"
|
|
- "Will pull: {{ 'YES' if (image_exists_before_pull.stdout | default('') == 'NOT_FOUND') else 'NO (already exists)' }}"
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
|
|
- name: Pull Docker image from registry using shell command
|
|
ansible.builtin.shell: |
|
|
docker pull {{ deploy_image }} 2>&1
|
|
when:
|
|
- registry_accessible is defined and registry_accessible == 'true'
|
|
register: image_pull_result
|
|
ignore_errors: yes
|
|
failed_when: false
|
|
changed_when: image_pull_result.rc == 0
|
|
|
|
- name: Display pull result
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Pull command exit code: {{ image_pull_result.rc | default('unknown') }}"
|
|
- "Pull stdout: {{ image_pull_result.stdout | default('none') }}"
|
|
- "Pull stderr: {{ image_pull_result.stderr | default('none') }}"
|
|
- "Pull succeeded: {{ 'YES' if (image_pull_result.rc | default(1) == 0) else 'NO' }}"
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
|
|
- name: Verify image exists locally after pull
|
|
community.docker.docker_image_info:
|
|
name: "{{ deploy_image }}"
|
|
register: image_info
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
ignore_errors: yes
|
|
failed_when: false
|
|
|
|
- name: Check if image exists by inspecting docker images
|
|
ansible.builtin.shell: |
|
|
docker images --format "{{ '{{' }}.Repository{{ '}}' }}:{{ '{{' }}.Tag{{ '}}' }}" | grep -E "^{{ deploy_image | regex_escape }}$" || echo "NOT_FOUND"
|
|
register: image_check
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Display image verification results
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Image info result: {{ image_info | default('not executed') }}"
|
|
- "Image check result: {{ image_check.stdout | default('not executed') }}"
|
|
- "Image exists: {{ 'YES' if (image_check.stdout | default('') != 'NOT_FOUND' and image_check.stdout | default('') != '') else 'NO' }}"
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
|
|
- name: Fail if image was not pulled successfully
|
|
ansible.builtin.fail:
|
|
msg: |
|
|
Failed to pull image {{ deploy_image }} from registry.
|
|
The image does not exist locally after pull attempt.
|
|
|
|
Pull command result:
|
|
- Exit code: {{ image_pull_result.rc | default('unknown') }}
|
|
- Stdout: {{ image_pull_result.stdout | default('none') }}
|
|
- Stderr: {{ image_pull_result.stderr | default('none') }}
|
|
|
|
Image check result: {{ image_check.stdout | default('unknown') }}
|
|
|
|
Please check:
|
|
1. Does the image exist in {{ source_registry }}?
|
|
2. Are registry credentials correct?
|
|
3. Is the registry accessible?
|
|
4. Check the pull command output above for specific error messages.
|
|
when:
|
|
- registry_accessible is defined and registry_accessible == 'true'
|
|
- (image_check.stdout | default('') == 'NOT_FOUND' or image_check.stdout | default('') == '')
|
|
|
|
- name: Tag image for local registry (if source and local registry differ)
|
|
community.docker.docker_image:
|
|
name: "{{ deploy_image }}"
|
|
repository: "{{ local_image }}"
|
|
tag: "{{ image_tag }}"
|
|
source: local
|
|
when:
|
|
- source_registry != local_registry
|
|
- image_check.stdout is defined
|
|
- image_check.stdout != 'NOT_FOUND'
|
|
- image_check.stdout != ''
|
|
register: image_tag_result
|
|
|
|
- name: Push image to local registry (if source and local registry differ)
|
|
community.docker.docker_image:
|
|
name: "{{ local_image }}"
|
|
push: true
|
|
source: local
|
|
when:
|
|
- source_registry != local_registry
|
|
- image_tag_result.changed | default(false)
|
|
ignore_errors: yes
|
|
failed_when: false
|
|
|
|
- name: Update docker-compose file with new image tag
|
|
ansible.builtin.replace:
|
|
path: "{{ application_code_dest }}/docker-compose.{{ application_compose_suffix }}"
|
|
regexp: '^(\s+image:\s+)({{ local_registry }}/{{ app_name }}:)(.*)$'
|
|
replace: '\1\2{{ image_tag }}'
|
|
register: compose_update_result
|
|
failed_when: false
|
|
changed_when: compose_update_result.changed | default(false)
|
|
|
|
- name: Update docker-compose file with new image (alternative pattern - any registry)
|
|
ansible.builtin.replace:
|
|
path: "{{ application_code_dest }}/docker-compose.{{ application_compose_suffix }}"
|
|
regexp: '^(\s+image:\s+)([^\/]+\/{{ app_name }}:)(.*)$'
|
|
replace: '\1{{ local_registry }}/{{ app_name }}:{{ image_tag }}'
|
|
register: compose_update_alt
|
|
when: compose_update_result.changed == false
|
|
failed_when: false
|
|
changed_when: compose_update_alt.changed | default(false)
|
|
|
|
- name: Ensure Docker networks exist
|
|
community.docker.docker_network:
|
|
name: "{{ item }}"
|
|
state: present
|
|
loop:
|
|
- traefik-public
|
|
- app-internal
|
|
ignore_errors: yes
|
|
|
|
- name: Check if .env file exists
|
|
stat:
|
|
path: "{{ application_code_dest }}/.env"
|
|
register: env_file_exists
|
|
|
|
- name: Create minimal .env file if it doesn't exist
|
|
copy:
|
|
dest: "{{ application_code_dest }}/.env"
|
|
content: |
|
|
# Minimal .env file for Docker Compose
|
|
# This file should be properly configured by the application setup playbook
|
|
DB_USERNAME={{ db_username | default('postgres') }}
|
|
DB_PASSWORD={{ db_password | default('') }}
|
|
MINIO_ROOT_USER={{ minio_root_user | default('minioadmin') }}
|
|
MINIO_ROOT_PASSWORD={{ minio_root_password | default('') }}
|
|
SECRETS_DIR={{ secrets_dir | default('./secrets') }}
|
|
GIT_REPOSITORY_URL={{ git_repository_url | default('') }}
|
|
GIT_TOKEN={{ git_token | default('') }}
|
|
GIT_USERNAME={{ git_username | default('') }}
|
|
GIT_PASSWORD={{ git_password | default('') }}
|
|
owner: "{{ ansible_user }}"
|
|
group: "{{ ansible_user }}"
|
|
mode: '0600'
|
|
when: not env_file_exists.stat.exists
|
|
become: yes
|
|
|
|
- name: Check if Docker daemon.json exists
|
|
stat:
|
|
path: /etc/docker/daemon.json
|
|
register: docker_daemon_json
|
|
become: yes
|
|
|
|
- name: Read existing Docker daemon.json
|
|
slurp:
|
|
src: /etc/docker/daemon.json
|
|
register: docker_daemon_config
|
|
when: docker_daemon_json.stat.exists
|
|
become: yes
|
|
changed_when: false
|
|
|
|
- name: Set Docker daemon configuration with insecure registry
|
|
set_fact:
|
|
docker_daemon_config_dict: "{{ docker_daemon_config.content | b64decode | from_json if (docker_daemon_json.stat.exists and docker_daemon_config.content is defined) else {} }}"
|
|
|
|
- name: Build insecure registries list
|
|
set_fact:
|
|
insecure_registries_list: >-
|
|
{%- set existing = docker_daemon_config_dict.get('insecure-registries', []) | list -%}
|
|
{%- set needed_registries = [local_registry] | list -%}
|
|
{%- set all_registries = (existing + needed_registries) | unique | list -%}
|
|
{{ all_registries }}
|
|
|
|
- name: Merge insecure registry into Docker daemon config
|
|
set_fact:
|
|
docker_daemon_config_merged: "{{ docker_daemon_config_dict | combine({'insecure-registries': insecure_registries_list}) }}"
|
|
|
|
- name: Update Docker daemon.json with insecure registry
|
|
copy:
|
|
dest: /etc/docker/daemon.json
|
|
content: "{{ docker_daemon_config_merged | to_json(indent=2) }}"
|
|
mode: '0644'
|
|
when: docker_daemon_config_merged != docker_daemon_config_dict
|
|
become: yes
|
|
register: docker_daemon_updated
|
|
|
|
- name: Restart Docker daemon if configuration changed
|
|
systemd:
|
|
name: docker
|
|
state: restarted
|
|
when: docker_daemon_updated.changed | default(false)
|
|
become: yes
|
|
ignore_errors: yes
|
|
|
|
- name: Wait for Docker daemon to be ready
|
|
shell: docker ps > /dev/null 2>&1
|
|
register: docker_ready
|
|
until: docker_ready.rc == 0
|
|
retries: 30
|
|
delay: 2
|
|
when: docker_daemon_updated.changed | default(false)
|
|
ignore_errors: yes
|
|
changed_when: false
|
|
|
|
- name: Set list of registries to login to (source registry for pulling, local registry for pushing)
|
|
ansible.builtin.set_fact:
|
|
registries_to_login: >-
|
|
{%- if source_registry is defined and local_registry is defined -%}
|
|
{%- if source_registry != local_registry -%}
|
|
{%- set reg_list = [source_registry, local_registry] -%}
|
|
{%- else -%}
|
|
{%- set reg_list = [local_registry] -%}
|
|
{%- endif -%}
|
|
{%- set final_list = reg_list | unique | list -%}
|
|
{{ final_list }}
|
|
{%- else -%}
|
|
{%- set default_list = [docker_registry | default('localhost:5000')] -%}
|
|
{{ default_list }}
|
|
{%- endif -%}
|
|
|
|
- name: Login to all Docker registries before compose up
|
|
community.docker.docker_login:
|
|
registry_url: "{{ item }}"
|
|
username: "{{ docker_registry_username | default('admin') }}"
|
|
password: "{{ registry_password }}"
|
|
when:
|
|
- registry_password | string | trim != ''
|
|
- registry_accessible == 'true'
|
|
loop: "{{ registries_to_login | default([docker_registry]) }}"
|
|
no_log: yes
|
|
register: docker_login_results
|
|
failed_when: false
|
|
|
|
- name: Display login results
|
|
ansible.builtin.debug:
|
|
msg: "Docker login to {{ item.item }}: {% if item.failed %}FAILED ({{ item.msg | default('unknown error') }}){% else %}SUCCESS{% endif %}"
|
|
when:
|
|
- registry_password | string | trim != ''
|
|
- registry_accessible == 'true'
|
|
loop: "{{ docker_login_results.results | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item.item }}"
|
|
|
|
- name: Deploy application stack with new image
|
|
shell: |
|
|
cd {{ application_code_dest }}
|
|
docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} up -d --pull missing --force-recreate --remove-orphans
|
|
register: compose_deploy_result
|
|
changed_when: true
|
|
environment:
|
|
DB_USERNAME: "{{ db_username | default('postgres') }}"
|
|
DB_PASSWORD: "{{ db_password | default('') }}"
|
|
MINIO_ROOT_USER: "{{ minio_root_user | default('minioadmin') }}"
|
|
MINIO_ROOT_PASSWORD: "{{ minio_root_password | default('') }}"
|
|
SECRETS_DIR: "{{ secrets_dir | default('./secrets') }}"
|
|
GIT_REPOSITORY_URL: "{{ git_repository_url | default('') }}"
|
|
GIT_BRANCH: "{{ git_branch | default('main') }}"
|
|
GIT_TOKEN: "{{ git_token | default('') }}"
|
|
GIT_USERNAME: "{{ git_username | default('') }}"
|
|
GIT_PASSWORD: "{{ git_password | default('') }}"
|
|
|
|
- name: Wait for containers to start
|
|
ansible.builtin.pause:
|
|
seconds: 15
|
|
|
|
- name: Check container status
|
|
shell: |
|
|
cd {{ application_code_dest }}
|
|
docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} ps
|
|
register: container_status
|
|
changed_when: false
|
|
|
|
- name: Display deployment summary
|
|
ansible.builtin.debug:
|
|
msg: |
|
|
========================================
|
|
Image Deployment Summary
|
|
========================================
|
|
Image: {{ deploy_image }}
|
|
Tag: {{ image_tag }}
|
|
Environment: {{ deployment_environment }}
|
|
Stack: {{ application_code_dest }}
|
|
Status: SUCCESS
|
|
========================================
|
|
|
|
Container Status:
|
|
{{ container_status.stdout | default('Not available') }}
|
|
========================================
|
|
|