fix: staging deployment configuration and redis secrets handling
This commit is contained in:
239
deployment/ansible/playbooks/deploy/production.yml
Normal file
239
deployment/ansible/playbooks/deploy/production.yml
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
- name: Deploy Application Update to Production via Docker Compose
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# These should be passed via -e from CI/CD
|
||||
application_environment: production
|
||||
application_compose_suffix: production.yml
|
||||
# app_stack_path is now defined in group_vars/production.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Set deployment variables
|
||||
set_fact:
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
git_commit_sha: "{{ git_commit_sha | default('unknown') }}"
|
||||
deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}"
|
||||
- name: Optionally load registry credentials from encrypted vault
|
||||
include_vars:
|
||||
file: "{{ playbook_dir }}/../../secrets/production.vault.yml"
|
||||
no_log: yes
|
||||
ignore_errors: yes
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
|
||||
- name: Derive docker registry credentials from vault when not provided
|
||||
set_fact:
|
||||
docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(docker_registry_username_default)) }}"
|
||||
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(docker_registry_password_default)) }}"
|
||||
|
||||
- name: Ensure system packages are up to date
|
||||
include_role:
|
||||
name: system
|
||||
when: system_update_packages | bool
|
||||
|
||||
- name: Verify Docker is running
|
||||
systemd:
|
||||
name: docker
|
||||
state: started
|
||||
register: docker_service
|
||||
become: yes
|
||||
|
||||
- name: Fail if Docker is not running
|
||||
fail:
|
||||
msg: "Docker service is not running"
|
||||
when: docker_service.status.ActiveState != 'active'
|
||||
|
||||
- name: Ensure application stack directory exists
|
||||
file:
|
||||
path: "{{ app_stack_path }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Check if docker-compose.base.yml exists in application stack
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
register: compose_base_exists
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Check if docker-compose.production.yml exists in application stack
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.production.yml"
|
||||
register: compose_override_exists
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Fail if docker-compose files don't exist
|
||||
fail:
|
||||
msg: |
|
||||
Application Stack docker-compose files not found at {{ app_stack_path }}
|
||||
|
||||
Required files:
|
||||
- docker-compose.base.yml
|
||||
- docker-compose.production.yml
|
||||
|
||||
The Application Stack must be deployed first via:
|
||||
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
|
||||
|
||||
This will create the application stack with docker-compose files and .env file.
|
||||
when:
|
||||
- not (application_sync_files | default(false) | bool)
|
||||
- (not compose_base_exists.stat.exists or not compose_override_exists.stat.exists)
|
||||
|
||||
- name: Create backup directory
|
||||
file:
|
||||
path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
|
||||
tasks:
|
||||
- name: Verify docker-compose files exist
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
register: compose_base_check
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Verify docker-compose.production.yml exists
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.production.yml"
|
||||
register: compose_override_check
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Fail if docker-compose files don't exist
|
||||
fail:
|
||||
msg: |
|
||||
Application Stack docker-compose files not found at {{ app_stack_path }}
|
||||
|
||||
Required files:
|
||||
- docker-compose.base.yml
|
||||
- docker-compose.production.yml
|
||||
|
||||
The Application Stack must be deployed first via:
|
||||
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
|
||||
|
||||
This will create the application stack with docker-compose files and .env file.
|
||||
when:
|
||||
- not (application_sync_files | default(false) | bool)
|
||||
- (not compose_base_check.stat.exists or not compose_override_check.stat.exists)
|
||||
|
||||
- name: Backup current deployment metadata
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.yml ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.yml config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
when:
|
||||
- not (application_sync_files | default(false) | bool)
|
||||
- compose_base_exists.stat.exists | default(false)
|
||||
- compose_override_exists.stat.exists | default(false)
|
||||
|
||||
- name: Login to Docker registry (if credentials provided)
|
||||
community.docker.docker_login:
|
||||
registry_url: "{{ docker_registry_url }}"
|
||||
username: "{{ docker_registry_username }}"
|
||||
password: "{{ docker_registry_password }}"
|
||||
no_log: yes
|
||||
ignore_errors: yes
|
||||
when:
|
||||
- docker_registry_username is defined
|
||||
- docker_registry_password is defined
|
||||
- docker_registry_username | length > 0
|
||||
- docker_registry_password | length > 0
|
||||
register: registry_login
|
||||
|
||||
- name: Pull new Docker image
|
||||
community.docker.docker_image:
|
||||
name: "{{ app_image }}"
|
||||
tag: "{{ image_tag }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
register: image_pull
|
||||
|
||||
- name: Verify image was pulled successfully
|
||||
fail:
|
||||
msg: "Failed to pull image {{ app_image }}:{{ image_tag }}"
|
||||
when: image_pull.failed
|
||||
|
||||
# Sync files first if application_sync_files=true (before updating docker-compose.production.yml)
|
||||
- name: Sync application stack files
|
||||
import_role:
|
||||
name: application
|
||||
vars:
|
||||
application_sync_files: "{{ application_sync_files | default(false) }}"
|
||||
application_compose_recreate: "never" # Don't recreate yet, just sync files
|
||||
application_remove_orphans: false
|
||||
when: application_sync_files | default(false) | bool
|
||||
|
||||
- name: Update docker-compose.production.yml with new image tag (all services)
|
||||
replace:
|
||||
path: "{{ app_stack_path }}/docker-compose.production.yml"
|
||||
# Match both localhost:5000 and registry.michaelschiemer.de (or any registry URL)
|
||||
regexp: '^(\s+image:\s+)(localhost:5000|registry\.michaelschiemer\.de|{{ docker_registry }})/{{ app_name }}:.*$'
|
||||
replace: '\1{{ app_image }}:{{ image_tag }}'
|
||||
# Always update to ensure localhost:5000 is used (registry only accessible via localhost)
|
||||
when: true
|
||||
register: compose_updated
|
||||
|
||||
- name: Redeploy application stack with new image
|
||||
import_role:
|
||||
name: application
|
||||
vars:
|
||||
application_sync_files: false # Already synced above, don't sync again
|
||||
application_compose_recreate: "always"
|
||||
application_remove_orphans: true
|
||||
|
||||
- name: Get deployed image information
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: deployed_image
|
||||
changed_when: false
|
||||
|
||||
- name: Record deployment metadata
|
||||
copy:
|
||||
content: |
|
||||
Deployment Timestamp: {{ deployment_timestamp }}
|
||||
Git Commit: {{ git_commit_sha }}
|
||||
Image Tag: {{ image_tag }}
|
||||
Deployed Image: {{ deployed_image.stdout }}
|
||||
Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }}
|
||||
Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}
|
||||
Health Status: {{ application_health_output if application_health_output != '' else 'All services healthy' }}
|
||||
Health Check HTTP Status: {{ application_healthcheck_status }}
|
||||
dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Cleanup old backups (keep last {{ max_rollback_versions | default(5) }})
|
||||
shell: |
|
||||
cd {{ backups_path }}
|
||||
ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions | default(5) + 1 }} | xargs -r rm -rf
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
post_tasks:
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "=== Production Deployment Summary ==="
|
||||
- "Image: {{ app_image }}:{{ image_tag }}"
|
||||
- "Commit: {{ git_commit_sha }}"
|
||||
- "Timestamp: {{ deployment_timestamp }}"
|
||||
- "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}"
|
||||
- "Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}"
|
||||
- "Health Output: {{ application_health_output if application_health_output != '' else 'All services healthy' }}"
|
||||
- "Health Check HTTP Status: {{ application_healthcheck_status }}"
|
||||
- "Health Check URL: {{ health_check_url | default('https://michaelschiemer.de/health') }}"
|
||||
- ""
|
||||
- "Next: Verify application is healthy"
|
||||
Reference in New Issue
Block a user