Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Successful in 38s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Successful in 14s
Security Vulnerability Scan / Composer Security Audit (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Successful in 14s
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Successful in 33s
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Failing after 58s
The previous regex was removing port numbers from registry URLs. Now using sed to only remove the image name part after the slash, preserving the full registry URL including port (e.g. git.michaelschiemer.de:5000)
333 lines
13 KiB
YAML
333 lines
13 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: Set deploy_image from registry, app_name and tag
|
|
ansible.builtin.set_fact:
|
|
deploy_image: "{{ docker_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: Pull Docker image
|
|
community.docker.docker_image:
|
|
name: "{{ deploy_image }}"
|
|
source: pull
|
|
pull: true
|
|
when: registry_accessible is defined and registry_accessible == 'true'
|
|
register: image_pull_result
|
|
ignore_errors: yes
|
|
failed_when: false
|
|
|
|
- name: Verify image exists locally
|
|
community.docker.docker_image_info:
|
|
name: "{{ deploy_image }}"
|
|
register: image_info
|
|
failed_when: image_info.failed | default(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+)({{ docker_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)
|
|
ansible.builtin.replace:
|
|
path: "{{ application_code_dest }}/docker-compose.{{ application_compose_suffix }}"
|
|
regexp: 'image:\s+{{ docker_registry }}/{{ app_name }}:.*'
|
|
replace: 'image: {{ deploy_image }}'
|
|
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 -%}
|
|
{%- if 'git.michaelschiemer.de:5000' not in existing -%}
|
|
{{ existing + ['git.michaelschiemer.de:5000'] }}
|
|
{%- else -%}
|
|
{{ existing }}
|
|
{%- endif -%}
|
|
|
|
- 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: Extract registry URLs with ports from docker-compose files
|
|
ansible.builtin.shell: |
|
|
cd {{ application_code_dest }}
|
|
grep -h "image:" docker-compose.base.yml docker-compose.{{ application_compose_suffix }} 2>/dev/null | \
|
|
sed -E 's/.*image:\s*([^\/]+).*/\1/' | \
|
|
sed -E 's/\/.*$//' | \
|
|
sort -u || echo ""
|
|
register: actual_registry_urls_full
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Set list of registries to login to (filter out service names, preserve ports)
|
|
ansible.builtin.set_fact:
|
|
registries_to_login: >-
|
|
{%- set found_registries = actual_registry_urls_full.stdout | trim | split('\n') | select('match', '.+') | list -%}
|
|
{%- set filtered_registries = [] -%}
|
|
{%- for reg in found_registries -%}
|
|
{%- if reg | regex_search('\.(de|com|org|net|io|dev)') or reg | regex_search(':[0-9]+') or reg == 'localhost' -%}
|
|
{%- set _ = filtered_registries.append(reg) -%}
|
|
{%- endif -%}
|
|
{%- endfor -%}
|
|
{%- set default_registry = [docker_registry] -%}
|
|
{%- if filtered_registries | length > 0 -%}
|
|
{{ filtered_registries | unique | list }}
|
|
{%- else -%}
|
|
{{ default_registry }}
|
|
{%- 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') }}
|
|
========================================
|
|
|