Files
michaelschiemer/deployment/ansible/playbooks/deploy-image.yml
Michael Schiemer c8ffb6e298
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
fix: preserve port numbers in Docker registry URL extraction
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)
2025-11-09 00:05:11 +01:00

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') }}
========================================