feat: Integrate Ansible playbooks into CI/CD workflows

- Add deploy-application-code.yml for Git-based code deployment
- Add install-composer-dependencies.yml for dependency installation
- Add deploy-image.yml for Docker image deployment
- Update build-image.yml to use Ansible playbooks
- Update manual-deploy.yml to use Ansible playbooks
- Add ANSIBLE_VAULT_PASSWORD secret handling
This commit is contained in:
2025-11-07 18:14:11 +01:00
parent cf903f2582
commit 1963b10749
10 changed files with 636 additions and 590 deletions

View File

@@ -1,93 +0,0 @@
---
# Production Deployment - Centralized Variables
# These variables are used across all playbooks
# System Maintenance
system_update_packages: true
system_apt_upgrade: dist
system_enable_unattended_upgrades: true
system_enable_unattended_reboot: false
system_unattended_reboot_time: "02:00"
system_enable_unattended_timer: true
system_enable_docker_prune: false
# Deployment Paths
deploy_user_home: "/home/deploy"
stacks_base_path: "{{ deploy_user_home }}/deployment/stacks"
app_stack_path: "{{ stacks_base_path }}/application"
backups_path: "{{ deploy_user_home }}/deployment/backups"
# Docker Registry
docker_registry: "localhost:5000"
docker_registry_url: "localhost:5000"
docker_registry_external: "registry.michaelschiemer.de"
docker_registry_username_default: "admin"
# docker_registry_password_default should be set in vault as vault_docker_registry_password
# If not using vault, override via -e docker_registry_password_default="your-password"
docker_registry_password_default: ""
registry_auth_path: "{{ stacks_base_path }}/registry/auth"
# Application Configuration
app_name: "framework"
app_domain: "michaelschiemer.de"
app_image: "{{ docker_registry }}/{{ app_name }}"
app_image_external: "{{ docker_registry_external }}/{{ app_name }}"
# Domain Configuration
gitea_domain: "git.michaelschiemer.de"
# Email Configuration
mail_from_address: "noreply@{{ app_domain }}"
acme_email: "kontakt@{{ app_domain }}"
# SSL Certificate Domains
ssl_domains:
- "{{ gitea_domain }}"
- "{{ app_domain }}"
# Health Check Configuration
health_check_url: "https://{{ app_domain }}/health"
health_check_retries: 10
health_check_delay: 10
# Rollback Configuration
max_rollback_versions: 5
rollback_timeout: 300
# Wait Timeouts
wait_timeout: 60
# Git Configuration (for sync-code.yml)
git_repository_url_default: "https://{{ gitea_domain }}/michael/michaelschiemer.git"
git_branch_default: "main"
git_token: "{{ vault_git_token | default('') }}"
git_username: "{{ vault_git_username | default('') }}"
git_password: "{{ vault_git_password | default('') }}"
# Database Configuration
db_user_default: "postgres"
db_name_default: "michaelschiemer"
# MinIO Object Storage Configuration
minio_root_user: "{{ vault_minio_root_user | default('minioadmin') }}"
minio_root_password: "{{ vault_minio_root_password | default('') }}"
minio_api_domain: "minio-api.michaelschiemer.de"
minio_console_domain: "minio.michaelschiemer.de"
# WireGuard Configuration
wireguard_interface: "wg0"
wireguard_config_path: "/etc/wireguard"
wireguard_port_default: 51820
wireguard_network_default: "10.8.0.0/24"
wireguard_server_ip_default: "10.8.0.1"
wireguard_enable_ip_forwarding: true
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
wireguard_private_key_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}_private.key"
wireguard_public_key_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}_public.key"
wireguard_client_configs_path: "{{ wireguard_config_path }}/clients"
# WireGuard DNS Configuration
# DNS server for VPN clients (points to VPN server IP)
# This ensures internal services are resolved to VPN IPs
wireguard_dns_servers:
- "{{ wireguard_server_ip_default }}"

View File

@@ -0,0 +1,123 @@
---
- name: Deploy Application Code via Git
hosts: "{{ deployment_hosts | default('production') }}"
gather_facts: yes
become: no
vars:
application_code_dest: "/home/deploy/michaelschiemer/current"
git_repository_url: "{{ git_repository_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}"
# Determine branch based on environment
git_branch: >-
{%- if deployment_environment == 'staging' -%}
{{ git_branch | default('staging') }}
{%- else -%}
{{ git_branch | default('main') }}
{%- endif -%}
git_token: "{{ git_token | default('') }}"
# Deployment environment (staging or production)
deployment_environment: "{{ deployment_environment | default('production') }}"
tasks:
- name: Ensure Git is installed
ansible.builtin.apt:
name: git
state: present
update_cache: no
become: yes
- name: Ensure application code directory exists
file:
path: "{{ application_code_dest }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0755'
become: yes
- name: Check if repository already exists
stat:
path: "{{ application_code_dest }}/.git"
register: git_repo_exists
- name: Clone repository (if not exists)
ansible.builtin.git:
repo: "{{ git_repository_url }}"
dest: "{{ application_code_dest }}"
version: "{{ git_branch }}"
force: no
update: no
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: not git_repo_exists.stat.exists
environment:
GIT_TERMINAL_PROMPT: "0"
vars:
ansible_become: no
- name: Update repository (if exists)
ansible.builtin.git:
repo: "{{ git_repository_url }}"
dest: "{{ application_code_dest }}"
version: "{{ git_branch }}"
force: yes
update: yes
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: git_repo_exists.stat.exists
environment:
GIT_TERMINAL_PROMPT: "0"
vars:
ansible_become: no
- name: Ensure executable permissions on PHP scripts
file:
path: "{{ application_code_dest }}/{{ item }}"
mode: '0755'
loop:
- worker.php
- console.php
ignore_errors: yes
- name: Verify worker.php exists
stat:
path: "{{ application_code_dest }}/worker.php"
register: worker_php_stat
- name: Verify console.php exists
stat:
path: "{{ application_code_dest }}/console.php"
register: console_php_stat
- name: Verify composer.json exists
stat:
path: "{{ application_code_dest }}/composer.json"
register: composer_json_stat
- name: Get current Git commit hash
shell: |
cd {{ application_code_dest }} && git rev-parse HEAD
register: git_commit_hash
changed_when: false
when: git_repo_exists.stat.exists
- name: Display file verification results
debug:
msg: |
File Verification:
- worker.php: {{ 'EXISTS' if worker_php_stat.stat.exists else 'MISSING' }}
- console.php: {{ 'EXISTS' if console_php_stat.stat.exists else 'MISSING' }}
- composer.json: {{ 'EXISTS' if composer_json_stat.stat.exists else 'MISSING' }}
- Git Branch: {{ git_branch }}
- Git Commit: {{ git_commit_hash.stdout | default('N/A') }}
- name: Fail if critical files are missing
fail:
msg: |
Critical files are missing after Git deployment:
{% if not worker_php_stat.stat.exists %}- worker.php{% endif %}
{% if not console_php_stat.stat.exists %}- console.php{% endif %}
{% if not composer_json_stat.stat.exists %}- composer.json{% endif %}
when:
- not worker_php_stat.stat.exists or not console_php_stat.stat.exists or not composer_json_stat.stat.exists

View File

@@ -0,0 +1,142 @@
---
- name: Deploy Docker Image to Application Stack
hosts: "{{ deployment_hosts | default('production') }}"
gather_facts: yes
become: no
vars:
# Determine stack path based on environment
application_stack_dest: >-
{%- if deployment_environment == 'staging' -%}
{{ staging_stack_path | default(stacks_base_path + '/staging') }}
{%- else -%}
{{ app_stack_path | default(stacks_base_path + '/production') }}
{%- endif -%}
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: "{{ app_name | default('framework') }}"
# Full image URL
deploy_image: "{{ docker_registry }}/{{ app_name }}:{{ image_tag }}"
# Deployment environment (staging or production)
deployment_environment: "{{ deployment_environment | default('production') }}"
tasks:
- 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: "http://{{ docker_registry }}/v2/"
method: GET
status_code: [200, 401]
timeout: 5
register: registry_check
ignore_errors: yes
delegate_to: "{{ inventory_hostname }}"
become: no
- 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_check.status | default(0) in [200, 401]
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
register: image_pull_result
failed_when: image_pull_result.failed | default(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_stack_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_stack_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: Deploy application stack with new image
shell: |
cd {{ application_stack_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
- name: Wait for containers to start
ansible.builtin.pause:
seconds: 15
- name: Check container status
shell: |
cd {{ application_stack_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_stack_dest }}
Status: SUCCESS
========================================
Container Status:
{{ container_status.stdout | default('Not available') }}
========================================

View File

@@ -0,0 +1,78 @@
---
- name: Install Composer Dependencies in Application Container
hosts: "{{ deployment_hosts | default('production') }}"
gather_facts: no
become: no
vars:
# Determine stack path based on environment
application_stack_dest: >-
{%- if deployment_environment == 'staging' -%}
{{ staging_stack_path | default(stacks_base_path + '/staging') }}
{%- else -%}
{{ app_stack_path | default(stacks_base_path + '/production') }}
{%- endif -%}
application_compose_suffix: >-
{%- if deployment_environment == 'staging' -%}
staging.yml
{%- else -%}
production.yml
{%- endif -%}
# Deployment environment (staging or production)
deployment_environment: "{{ deployment_environment | default('production') }}"
# Service name (php for production, staging-app for staging)
php_service_name: >-
{%- if deployment_environment == 'staging' -%}
staging-app
{%- else -%}
php
{%- endif -%}
tasks:
- name: Check if composer.json exists
stat:
path: /home/deploy/michaelschiemer/current/composer.json
delegate_to: "{{ inventory_hostname }}"
register: composer_json_exists
- name: Fail if composer.json is missing
fail:
msg: "composer.json not found at /home/deploy/michaelschiemer/current/composer.json"
when: not composer_json_exists.stat.exists
- name: Install composer dependencies in PHP container
shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ php_service_name }} composer install --no-dev --optimize-autoloader --no-interaction
register: composer_install
changed_when: true
failed_when: composer_install.rc != 0
- name: Restart queue-worker and scheduler to pick up vendor directory (production only)
shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} restart queue-worker scheduler
register: restart_workers
changed_when: true
failed_when: false
when: deployment_environment == 'production'
- name: Display composer install output
debug:
msg: |
Composer Install Output:
{{ composer_install.stdout }}
- name: Verify vendor/autoload.php exists
shell: |
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ php_service_name }} test -f /var/www/html/vendor/autoload.php && echo "EXISTS" || echo "MISSING"
register: autoload_check
changed_when: false
- name: Display autoload verification
debug:
msg: "vendor/autoload.php: {{ autoload_check.stdout.strip() }}"
- name: Fail if autoload.php is missing
fail:
msg: "vendor/autoload.php was not created after composer install"
when: "autoload_check.stdout.strip() != 'EXISTS'"