Files
michaelschiemer/deployment/ansible/playbooks/deploy-image.yml
Michael Schiemer bb5284220f
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Successful in 33s
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Failing after 1m8s
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Successful in 29s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Successful in 9s
🚀 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
fix: remove duplicate changed_when line
2025-11-09 00:21:08 +01:00

343 lines
14 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 "{{ docker_registry }}"
register: compose_registry_url
changed_when: false
failed_when: false
- name: Set actual registry from compose file or use default
ansible.builtin.set_fact:
actual_registry: "{{ (compose_registry_url.stdout | trim) if (compose_registry_url.stdout | trim != '' and compose_registry_url.stdout | trim != docker_registry) else docker_registry }}"
- name: Set deploy_image from actual registry, app_name and tag
ansible.builtin.set_fact:
deploy_image: "{{ actual_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+)({{ actual_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{{ actual_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 -%}
{%- 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 = found_registries | select('match', '.*\.(de|com|org|net|io|dev)(:[0-9]+)?$|^[^:]+:[0-9]+$|^localhost(:[0-9]+)?$') | list -%}
{%- 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') }}
========================================