feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution - Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments - Add force_deploy flag support in Ansible playbook to override stale locks - Use PHP deployment module as orchestrator (php console.php deploy:production) - Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal Architecture: - BuildStage → AnsibleDeployStage → HealthCheckStage for production - Process module provides timeout, error handling, and output capture - Ansible playbook supports rollback via rollback-git-based.yml - Zero-downtime deployments with health checks
This commit is contained in:
383
deployment/infrastructure/playbooks/deploy-git-based.yml
Normal file
383
deployment/infrastructure/playbooks/deploy-git-based.yml
Normal file
@@ -0,0 +1,383 @@
|
||||
---
|
||||
# Git-based Deployment Playbook with Releases/Symlink Pattern
|
||||
# Implements production-ready deployment with zero-downtime and rollback support
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml --extra-vars "git_branch=main"
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-git-based.yml --extra-vars "release_tag=v1.0.0"
|
||||
|
||||
- name: Deploy Custom PHP Framework (Git-based with Releases)
|
||||
hosts: web_servers
|
||||
become: true
|
||||
|
||||
vars:
|
||||
# Application configuration
|
||||
app_name: michaelschiemer
|
||||
app_user: deploy
|
||||
app_group: deploy
|
||||
|
||||
# Deployment paths
|
||||
app_base_path: "/var/www/{{ app_name }}"
|
||||
releases_path: "{{ app_base_path }}/releases"
|
||||
shared_path: "{{ app_base_path }}/shared"
|
||||
current_path: "{{ app_base_path }}/current"
|
||||
|
||||
# Git configuration
|
||||
git_repo: "https://github.com/michaelschiemer/michaelschiemer.git"
|
||||
git_branch: "{{ release_tag | default('main') }}"
|
||||
|
||||
# Release configuration
|
||||
release_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
release_name: "{{ release_tag | default(release_timestamp) }}"
|
||||
release_path: "{{ releases_path }}/{{ release_name }}"
|
||||
|
||||
# Deployment settings
|
||||
keep_releases: 5
|
||||
composer_install_flags: "--no-dev --optimize-autoloader --no-interaction"
|
||||
|
||||
# Shared directories and files
|
||||
shared_dirs:
|
||||
- storage/logs
|
||||
- storage/cache
|
||||
- storage/sessions
|
||||
- storage/uploads
|
||||
- public/uploads
|
||||
|
||||
shared_files:
|
||||
- .env.production
|
||||
|
||||
pre_tasks:
|
||||
- name: Check if deployment lock exists
|
||||
stat:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
register: deploy_lock
|
||||
|
||||
- name: Fail if deployment is already in progress
|
||||
fail:
|
||||
msg: "Deployment already in progress. Lock file exists: {{ app_base_path }}/.deploy.lock"
|
||||
when: deploy_lock.stat.exists
|
||||
|
||||
- name: Create deployment lock
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: touch
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Log deployment start
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Deployment started - Release: {{ release_name }} - User: {{ ansible_user_id }}"
|
||||
create: yes
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
|
||||
tasks:
|
||||
# ==========================================
|
||||
# 1. Directory Structure Setup
|
||||
# ==========================================
|
||||
|
||||
- name: Create base application directory
|
||||
file:
|
||||
path: "{{ app_base_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create releases directory
|
||||
file:
|
||||
path: "{{ releases_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create shared directory
|
||||
file:
|
||||
path: "{{ shared_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create shared subdirectories
|
||||
file:
|
||||
path: "{{ shared_path }}/{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
loop: "{{ shared_dirs }}"
|
||||
|
||||
# ==========================================
|
||||
# 2. Git Repository Clone
|
||||
# ==========================================
|
||||
|
||||
- name: Clone repository to new release directory
|
||||
git:
|
||||
repo: "{{ git_repo }}"
|
||||
dest: "{{ release_path }}"
|
||||
version: "{{ git_branch }}"
|
||||
force: yes
|
||||
depth: 1
|
||||
become_user: "{{ app_user }}"
|
||||
register: git_clone
|
||||
|
||||
- name: Get current commit hash
|
||||
command: git rev-parse HEAD
|
||||
args:
|
||||
chdir: "{{ release_path }}"
|
||||
register: commit_hash
|
||||
changed_when: false
|
||||
|
||||
- name: Log commit hash
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Commit: {{ commit_hash.stdout }}"
|
||||
|
||||
# ==========================================
|
||||
# 3. Shared Files/Directories Symlinks
|
||||
# ==========================================
|
||||
|
||||
- name: Remove shared directories from release (they will be symlinked)
|
||||
file:
|
||||
path: "{{ release_path }}/{{ item }}"
|
||||
state: absent
|
||||
loop: "{{ shared_dirs }}"
|
||||
|
||||
- name: Create symlinks for shared directories
|
||||
file:
|
||||
src: "{{ shared_path }}/{{ item }}"
|
||||
dest: "{{ release_path }}/{{ item }}"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
loop: "{{ shared_dirs }}"
|
||||
|
||||
- name: Create symlinks for shared files
|
||||
file:
|
||||
src: "{{ shared_path }}/{{ item }}"
|
||||
dest: "{{ release_path }}/{{ item }}"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
loop: "{{ shared_files }}"
|
||||
when: shared_files | length > 0
|
||||
|
||||
# ==========================================
|
||||
# 4. Dependencies Installation
|
||||
# ==========================================
|
||||
|
||||
- name: Install Composer dependencies
|
||||
composer:
|
||||
command: install
|
||||
arguments: "{{ composer_install_flags }}"
|
||||
working_dir: "{{ release_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
environment:
|
||||
COMPOSER_HOME: "/home/{{ app_user }}/.composer"
|
||||
|
||||
- name: Check if package.json exists
|
||||
stat:
|
||||
path: "{{ release_path }}/package.json"
|
||||
register: package_json
|
||||
|
||||
- name: Install NPM dependencies and build assets
|
||||
block:
|
||||
- name: Install NPM dependencies
|
||||
npm:
|
||||
path: "{{ release_path }}"
|
||||
state: present
|
||||
production: yes
|
||||
become_user: "{{ app_user }}"
|
||||
|
||||
- name: Build production assets
|
||||
command: npm run build
|
||||
args:
|
||||
chdir: "{{ release_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
when: package_json.stat.exists
|
||||
|
||||
# ==========================================
|
||||
# 5. File Permissions
|
||||
# ==========================================
|
||||
|
||||
- name: Set correct ownership for release
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
recurse: yes
|
||||
|
||||
- name: Make console script executable
|
||||
file:
|
||||
path: "{{ release_path }}/console.php"
|
||||
mode: '0755'
|
||||
ignore_errors: yes
|
||||
|
||||
# ==========================================
|
||||
# 6. Database Migrations (Optional)
|
||||
# ==========================================
|
||||
|
||||
- name: Run database migrations
|
||||
command: php console.php db:migrate --no-interaction
|
||||
args:
|
||||
chdir: "{{ release_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
when: run_migrations | default(false) | bool
|
||||
register: migrations_result
|
||||
|
||||
- name: Log migration result
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Migrations: {{ migrations_result.stdout | default('skipped') }}"
|
||||
when: run_migrations | default(false) | bool
|
||||
|
||||
# ==========================================
|
||||
# 7. Symlink Switch (Zero-Downtime)
|
||||
# ==========================================
|
||||
|
||||
- name: Get current release (before switch)
|
||||
stat:
|
||||
path: "{{ current_path }}"
|
||||
register: current_release_before
|
||||
|
||||
- name: Store previous release path for rollback
|
||||
set_fact:
|
||||
previous_release: "{{ current_release_before.stat.lnk_source | default('none') }}"
|
||||
|
||||
- name: Switch current symlink to new release (atomic operation)
|
||||
file:
|
||||
src: "{{ release_path }}"
|
||||
dest: "{{ current_path }}"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
force: yes
|
||||
|
||||
- name: Log symlink switch
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Symlink switched: {{ current_path }} -> {{ release_path }}"
|
||||
|
||||
# ==========================================
|
||||
# 8. Health Checks
|
||||
# ==========================================
|
||||
|
||||
- name: Wait for application to be ready
|
||||
wait_for:
|
||||
timeout: 10
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Health check - Summary endpoint
|
||||
uri:
|
||||
url: "http://{{ ansible_host }}/health/summary"
|
||||
method: GET
|
||||
return_content: yes
|
||||
status_code: 200
|
||||
register: health_check
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: health_check.status == 200
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Log health check result
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Health check: {{ health_check.status | default('FAILED') }}"
|
||||
|
||||
- name: Rollback on health check failure
|
||||
block:
|
||||
- name: Switch symlink back to previous release
|
||||
file:
|
||||
src: "{{ previous_release }}"
|
||||
dest: "{{ current_path }}"
|
||||
state: link
|
||||
force: yes
|
||||
when: previous_release != 'none'
|
||||
|
||||
- name: Remove failed release
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
state: absent
|
||||
|
||||
- name: Log rollback
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] ROLLBACK: Health check failed, reverted to {{ previous_release }}"
|
||||
|
||||
- name: Fail deployment
|
||||
fail:
|
||||
msg: "Deployment failed - health check returned {{ health_check.status }}. Rolled back to previous release."
|
||||
when: health_check.status != 200
|
||||
|
||||
# ==========================================
|
||||
# 9. Cleanup Old Releases
|
||||
# ==========================================
|
||||
|
||||
- name: Get list of all releases
|
||||
find:
|
||||
paths: "{{ releases_path }}"
|
||||
file_type: directory
|
||||
register: all_releases
|
||||
|
||||
- name: Sort releases by creation time
|
||||
set_fact:
|
||||
sorted_releases: "{{ all_releases.files | sort(attribute='ctime', reverse=true) }}"
|
||||
|
||||
- name: Remove old releases (keep last {{ keep_releases }})
|
||||
file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ sorted_releases[keep_releases:] }}"
|
||||
when: sorted_releases | length > keep_releases
|
||||
|
||||
- name: Log cleanup
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Cleanup: Kept {{ [sorted_releases | length, keep_releases] | min }} releases, removed {{ [sorted_releases | length - keep_releases, 0] | max }}"
|
||||
|
||||
post_tasks:
|
||||
- name: Cleanup and logging
|
||||
block:
|
||||
- name: Remove deployment lock
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: absent
|
||||
|
||||
- name: Log deployment completion
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Deployment completed successfully - Release: {{ release_name }}"
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "=========================================="
|
||||
- "Deployment Summary"
|
||||
- "=========================================="
|
||||
- "Release: {{ release_name }}"
|
||||
- "Commit: {{ commit_hash.stdout }}"
|
||||
- "Path: {{ release_path }}"
|
||||
- "Current: {{ current_path }}"
|
||||
- "Health Check: {{ health_check.status | default('N/A') }}"
|
||||
- "Previous Release: {{ previous_release }}"
|
||||
- "=========================================="
|
||||
|
||||
rescue:
|
||||
- name: Remove deployment lock on failure
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: absent
|
||||
|
||||
- name: Log deployment failure
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] DEPLOYMENT FAILED - Release: {{ release_name }}"
|
||||
|
||||
- name: Fail with error message
|
||||
fail:
|
||||
msg: "Deployment failed. Check {{ app_base_path }}/deploy.log for details."
|
||||
472
deployment/infrastructure/playbooks/deploy-rsync-based.yml
Normal file
472
deployment/infrastructure/playbooks/deploy-rsync-based.yml
Normal file
@@ -0,0 +1,472 @@
|
||||
---
|
||||
# Rsync-based Deployment Playbook with Releases/Symlink Pattern
|
||||
# Implements production-ready deployment with zero-downtime and rollback support
|
||||
# No GitHub dependency - deploys directly from local machine
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/deploy-rsync-based.yml --extra-vars "release_tag=v1.0.0"
|
||||
|
||||
- name: Deploy Custom PHP Framework (Rsync-based with Releases)
|
||||
hosts: web_servers
|
||||
become: true
|
||||
|
||||
vars:
|
||||
# Application configuration
|
||||
app_name: michaelschiemer
|
||||
app_user: deploy
|
||||
app_group: deploy
|
||||
|
||||
# Deployment paths
|
||||
app_base_path: "/home/{{ app_user }}/{{ app_name }}"
|
||||
releases_path: "{{ app_base_path }}/releases"
|
||||
shared_path: "{{ app_base_path }}/shared"
|
||||
current_path: "{{ app_base_path }}/current"
|
||||
|
||||
# Local source directory (project root on your machine)
|
||||
local_project_path: "{{ playbook_dir }}/../../.."
|
||||
|
||||
# Release configuration
|
||||
release_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
release_name: "{{ release_tag | default(release_timestamp) }}"
|
||||
release_path: "{{ releases_path }}/{{ release_name }}"
|
||||
|
||||
# Deployment settings
|
||||
keep_releases: 5
|
||||
composer_install_flags: "--no-dev --optimize-autoloader --no-interaction"
|
||||
|
||||
# Shared directories and files
|
||||
# Shared directories that need symlinks
|
||||
# NOTE: storage/logs, storage/cache, storage/uploads are handled by Docker volumes
|
||||
shared_dirs:
|
||||
- storage/sessions
|
||||
- public/uploads
|
||||
|
||||
shared_files:
|
||||
- .env.production
|
||||
|
||||
# Rsync exclusions
|
||||
rsync_excludes:
|
||||
- .git/
|
||||
- .github/
|
||||
- node_modules/
|
||||
- .env
|
||||
- .env.local
|
||||
- .env.development
|
||||
- storage/
|
||||
- public/uploads/
|
||||
- tests/
|
||||
- .idea/
|
||||
- .vscode/
|
||||
- "*.log"
|
||||
- .DS_Store
|
||||
- deployment/
|
||||
- database.sqlite
|
||||
- "*.cache"
|
||||
- .php-cs-fixer.cache
|
||||
- var/cache/
|
||||
- var/logs/
|
||||
|
||||
pre_tasks:
|
||||
- name: Install Composer dependencies locally before deployment
|
||||
local_action:
|
||||
module: command
|
||||
cmd: composer install {{ composer_install_flags }}
|
||||
chdir: "{{ local_project_path }}"
|
||||
become: false
|
||||
|
||||
- name: Build NPM assets locally before deployment
|
||||
local_action:
|
||||
module: command
|
||||
cmd: npm run build
|
||||
chdir: "{{ local_project_path }}"
|
||||
become: false
|
||||
- name: Check if deployment lock exists
|
||||
stat:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
register: deploy_lock
|
||||
|
||||
- name: Remove stale deployment lock if force flag is set
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: absent
|
||||
when: deploy_lock.stat.exists and (force_deploy | default(false))
|
||||
|
||||
- name: Fail if deployment is already in progress (without force)
|
||||
fail:
|
||||
msg: "Deployment already in progress. Lock file exists: {{ app_base_path }}/.deploy.lock. Use --extra-vars 'force_deploy=true' to override."
|
||||
when: deploy_lock.stat.exists and not (force_deploy | default(false))
|
||||
|
||||
- name: Create deployment lock
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: touch
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Log deployment start
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Deployment started - Release: {{ release_name }} - User: {{ ansible_user_id }}"
|
||||
create: yes
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
|
||||
tasks:
|
||||
# ==========================================
|
||||
# 1. Directory Structure Setup
|
||||
# ==========================================
|
||||
|
||||
- name: Create base application directory
|
||||
file:
|
||||
path: "{{ app_base_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create releases directory
|
||||
file:
|
||||
path: "{{ releases_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create shared directory
|
||||
file:
|
||||
path: "{{ shared_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create shared subdirectories
|
||||
file:
|
||||
path: "{{ shared_path }}/{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
loop: "{{ shared_dirs }}"
|
||||
|
||||
# ==========================================
|
||||
# 2. Rsync Application Code to New Release
|
||||
# ==========================================
|
||||
|
||||
- name: Create new release directory
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Sync application code to new release via rsync
|
||||
synchronize:
|
||||
src: "{{ local_project_path }}/"
|
||||
dest: "{{ release_path }}/"
|
||||
delete: yes
|
||||
recursive: yes
|
||||
rsync_opts: "{{ rsync_excludes | map('regex_replace', '^(.*)$', '--exclude=\\1') | list }}"
|
||||
private_key: "{{ ansible_ssh_private_key_file }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
- name: Set correct ownership for release
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
recurse: yes
|
||||
|
||||
- name: Get local git commit hash (if available)
|
||||
command: git rev-parse HEAD
|
||||
args:
|
||||
chdir: "{{ local_project_path }}"
|
||||
register: commit_hash
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Log commit hash
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Commit: {{ commit_hash.stdout | default('N/A - not a git repository') }}"
|
||||
when: commit_hash.rc == 0
|
||||
|
||||
# ==========================================
|
||||
# 3. Shared Files/Directories Symlinks
|
||||
# ==========================================
|
||||
|
||||
- name: Remove shared directories from release (they will be symlinked)
|
||||
file:
|
||||
path: "{{ release_path }}/{{ item }}"
|
||||
state: absent
|
||||
loop: "{{ shared_dirs }}"
|
||||
|
||||
- name: Create parent directories for symlinks
|
||||
file:
|
||||
path: "{{ release_path }}/{{ item | dirname }}"
|
||||
state: directory
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
mode: '0755'
|
||||
loop: "{{ shared_dirs }}"
|
||||
# Skip if dirname is current directory ('.')
|
||||
when: (item | dirname) != '.'
|
||||
|
||||
- name: Create symlinks for shared directories
|
||||
file:
|
||||
src: "{{ shared_path }}/{{ item }}"
|
||||
dest: "{{ release_path }}/{{ item }}"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
force: yes
|
||||
loop: "{{ shared_dirs }}"
|
||||
|
||||
- name: Remove .env.production from release (will be symlinked)
|
||||
file:
|
||||
path: "{{ release_path }}/.env.production"
|
||||
state: absent
|
||||
|
||||
- name: Create symlink for .env.production
|
||||
file:
|
||||
src: "{{ shared_path }}/.env.production"
|
||||
dest: "{{ release_path }}/.env.production"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
force: yes
|
||||
|
||||
- name: Create .env symlink with relative path to shared .env.production for Docker container access
|
||||
file:
|
||||
src: "../../shared/.env.production"
|
||||
dest: "{{ release_path }}/.env"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
force: yes
|
||||
|
||||
# ==========================================
|
||||
# 4. Dependencies Installation
|
||||
# ==========================================
|
||||
|
||||
# Composer dependencies and NPM assets are already built locally and rsync'd
|
||||
# No need to run composer install or npm build on the server
|
||||
|
||||
# ==========================================
|
||||
# 5. File Permissions
|
||||
# ==========================================
|
||||
|
||||
- name: Make console script executable
|
||||
file:
|
||||
path: "{{ release_path }}/console.php"
|
||||
mode: '0755'
|
||||
ignore_errors: yes
|
||||
|
||||
# ==========================================
|
||||
# 6. Database Migrations (Optional)
|
||||
# ==========================================
|
||||
|
||||
- name: Run database migrations
|
||||
command: php console.php db:migrate --no-interaction
|
||||
args:
|
||||
chdir: "{{ release_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
when: run_migrations | default(false) | bool
|
||||
register: migrations_result
|
||||
|
||||
- name: Log migration result
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Migrations: {{ migrations_result.stdout | default('skipped') }}"
|
||||
when: run_migrations | default(false) | bool
|
||||
|
||||
# ==========================================
|
||||
# 7. Prepare for Deployment
|
||||
# ==========================================
|
||||
|
||||
- name: Get current release (before switch)
|
||||
stat:
|
||||
path: "{{ current_path }}"
|
||||
register: current_release_before
|
||||
|
||||
- name: Stop existing Docker containers (if any)
|
||||
command: docker compose down
|
||||
args:
|
||||
chdir: "{{ current_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
when: current_release_before.stat.exists
|
||||
ignore_errors: yes
|
||||
|
||||
# ==========================================
|
||||
# 8. Symlink Switch (Zero-Downtime)
|
||||
# ==========================================
|
||||
|
||||
- name: Store previous release path for rollback
|
||||
set_fact:
|
||||
previous_release: "{{ current_release_before.stat.lnk_source | default('none') }}"
|
||||
|
||||
- name: Switch current symlink to new release (atomic operation)
|
||||
file:
|
||||
src: "{{ release_path }}"
|
||||
dest: "{{ current_path }}"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
force: yes
|
||||
|
||||
- name: Log symlink switch
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Symlink switched: {{ current_path }} -> {{ release_path }}"
|
||||
|
||||
# ==========================================
|
||||
# 9. Start Docker Containers
|
||||
# ==========================================
|
||||
|
||||
- name: Start Docker containers with new release
|
||||
command: docker compose -f docker-compose.yml -f docker-compose.production.yml up -d --build
|
||||
args:
|
||||
chdir: "{{ current_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
|
||||
- name: Wait for containers to be ready
|
||||
pause:
|
||||
seconds: 15
|
||||
|
||||
# ==========================================
|
||||
# 10. Health Checks
|
||||
# ==========================================
|
||||
|
||||
- name: Wait for application to be ready
|
||||
wait_for:
|
||||
timeout: 10
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Health check - Summary endpoint
|
||||
uri:
|
||||
url: "http://{{ ansible_host }}/health/summary"
|
||||
method: GET
|
||||
return_content: yes
|
||||
status_code: 200
|
||||
register: health_check
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: health_check.status == 200
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Log health check result
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Health check: {{ health_check.status | default('FAILED') }}"
|
||||
|
||||
- name: Rollback on health check failure
|
||||
block:
|
||||
- name: Stop failed release containers
|
||||
command: docker compose down
|
||||
args:
|
||||
chdir: "{{ current_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
|
||||
- name: Switch symlink back to previous release
|
||||
file:
|
||||
src: "{{ previous_release }}"
|
||||
dest: "{{ current_path }}"
|
||||
state: link
|
||||
force: yes
|
||||
when: previous_release != 'none'
|
||||
|
||||
- name: Start previous release containers
|
||||
command: docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
|
||||
args:
|
||||
chdir: "{{ current_path }}"
|
||||
become_user: "{{ app_user }}"
|
||||
when: previous_release != 'none'
|
||||
|
||||
- name: Remove failed release
|
||||
file:
|
||||
path: "{{ release_path }}"
|
||||
state: absent
|
||||
|
||||
- name: Log rollback
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] ROLLBACK: Health check failed, reverted to {{ previous_release }}"
|
||||
|
||||
- name: Fail deployment
|
||||
fail:
|
||||
msg: "Deployment failed - health check returned {{ health_check.status }}. Rolled back to previous release."
|
||||
when: health_check.status != 200
|
||||
|
||||
# ==========================================
|
||||
# 11. Cleanup Old Releases
|
||||
# ==========================================
|
||||
|
||||
- name: Get list of all releases
|
||||
find:
|
||||
paths: "{{ releases_path }}"
|
||||
file_type: directory
|
||||
register: all_releases
|
||||
|
||||
- name: Sort releases by creation time
|
||||
set_fact:
|
||||
sorted_releases: "{{ all_releases.files | sort(attribute='ctime', reverse=true) }}"
|
||||
|
||||
- name: Remove old releases (keep last {{ keep_releases }})
|
||||
file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ sorted_releases[keep_releases:] }}"
|
||||
when: sorted_releases | length > keep_releases
|
||||
|
||||
- name: Log cleanup
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Cleanup: Kept {{ [sorted_releases | length, keep_releases] | min }} releases, removed {{ [sorted_releases | length - keep_releases, 0] | max }}"
|
||||
|
||||
post_tasks:
|
||||
- name: Cleanup and logging
|
||||
block:
|
||||
- name: Remove deployment lock
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: absent
|
||||
|
||||
- name: Log deployment completion
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] Deployment completed successfully - Release: {{ release_name }}"
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "=========================================="
|
||||
- "Deployment Summary"
|
||||
- "=========================================="
|
||||
- "Release: {{ release_name }}"
|
||||
- "Commit: {{ commit_hash.stdout | default('N/A') }}"
|
||||
- "Path: {{ release_path }}"
|
||||
- "Current: {{ current_path }}"
|
||||
- "Health Check: {{ health_check.status | default('N/A') }}"
|
||||
- "Previous Release: {{ previous_release }}"
|
||||
- "=========================================="
|
||||
|
||||
rescue:
|
||||
- name: Remove deployment lock on failure
|
||||
file:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
state: absent
|
||||
|
||||
- name: Log deployment failure
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] DEPLOYMENT FAILED - Release: {{ release_name }}"
|
||||
|
||||
- name: Fail with error message
|
||||
fail:
|
||||
msg: "Deployment failed. Check {{ app_base_path }}/deploy.log for details."
|
||||
142
deployment/infrastructure/playbooks/rollback-git-based.yml
Normal file
142
deployment/infrastructure/playbooks/rollback-git-based.yml
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
# Git-based Rollback Playbook
|
||||
# Rolls back to the previous release by switching the symlink
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/rollback-git-based.yml
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/rollback-git-based.yml --extra-vars "rollback_to=20241025123456"
|
||||
|
||||
- name: Rollback Custom PHP Framework (Git-based)
|
||||
hosts: web_servers
|
||||
become: true
|
||||
|
||||
vars:
|
||||
app_name: michaelschiemer
|
||||
app_user: deploy
|
||||
app_group: deploy
|
||||
app_base_path: "/var/www/{{ app_name }}"
|
||||
releases_path: "{{ app_base_path }}/releases"
|
||||
current_path: "{{ app_base_path }}/current"
|
||||
|
||||
pre_tasks:
|
||||
- name: Check if deployment lock exists
|
||||
stat:
|
||||
path: "{{ app_base_path }}/.deploy.lock"
|
||||
register: deploy_lock
|
||||
|
||||
- name: Fail if deployment is in progress
|
||||
fail:
|
||||
msg: "Cannot rollback - deployment in progress"
|
||||
when: deploy_lock.stat.exists
|
||||
|
||||
- name: Create rollback lock
|
||||
file:
|
||||
path: "{{ app_base_path }}/.rollback.lock"
|
||||
state: touch
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
|
||||
tasks:
|
||||
- name: Get current release
|
||||
stat:
|
||||
path: "{{ current_path }}"
|
||||
register: current_release
|
||||
|
||||
- name: Fail if no current release exists
|
||||
fail:
|
||||
msg: "No current release found at {{ current_path }}"
|
||||
when: not current_release.stat.exists
|
||||
|
||||
- name: Get list of all releases
|
||||
find:
|
||||
paths: "{{ releases_path }}"
|
||||
file_type: directory
|
||||
register: all_releases
|
||||
|
||||
- name: Sort releases by creation time (newest first)
|
||||
set_fact:
|
||||
sorted_releases: "{{ all_releases.files | sort(attribute='ctime', reverse=true) }}"
|
||||
|
||||
- name: Determine target release for rollback
|
||||
set_fact:
|
||||
target_release: "{{ rollback_to if rollback_to is defined else sorted_releases[1].path }}"
|
||||
|
||||
- name: Verify target release exists
|
||||
stat:
|
||||
path: "{{ target_release }}"
|
||||
register: target_release_stat
|
||||
|
||||
- name: Fail if target release doesn't exist
|
||||
fail:
|
||||
msg: "Target release not found: {{ target_release }}"
|
||||
when: not target_release_stat.stat.exists
|
||||
|
||||
- name: Display rollback information
|
||||
debug:
|
||||
msg:
|
||||
- "Current release: {{ current_release.stat.lnk_source }}"
|
||||
- "Rolling back to: {{ target_release }}"
|
||||
|
||||
- name: Switch symlink to previous release
|
||||
file:
|
||||
src: "{{ target_release }}"
|
||||
dest: "{{ current_path }}"
|
||||
state: link
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_group }}"
|
||||
force: yes
|
||||
|
||||
- name: Wait for application to be ready
|
||||
wait_for:
|
||||
timeout: 5
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Health check after rollback
|
||||
uri:
|
||||
url: "http://{{ ansible_host }}/health/summary"
|
||||
method: GET
|
||||
return_content: yes
|
||||
status_code: 200
|
||||
register: health_check
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: health_check.status == 200
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Log rollback
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] ROLLBACK: {{ current_release.stat.lnk_source }} -> {{ target_release }} - Health: {{ health_check.status | default('FAILED') }}"
|
||||
create: yes
|
||||
|
||||
- name: Display rollback result
|
||||
debug:
|
||||
msg:
|
||||
- "=========================================="
|
||||
- "Rollback completed"
|
||||
- "Previous: {{ current_release.stat.lnk_source }}"
|
||||
- "Current: {{ target_release }}"
|
||||
- "Health check: {{ health_check.status | default('FAILED') }}"
|
||||
- "=========================================="
|
||||
|
||||
post_tasks:
|
||||
- name: Remove rollback lock
|
||||
file:
|
||||
path: "{{ app_base_path }}/.rollback.lock"
|
||||
state: absent
|
||||
|
||||
rescue:
|
||||
- name: Remove rollback lock on failure
|
||||
file:
|
||||
path: "{{ app_base_path }}/.rollback.lock"
|
||||
state: absent
|
||||
|
||||
- name: Log rollback failure
|
||||
lineinfile:
|
||||
path: "{{ app_base_path }}/deploy.log"
|
||||
line: "[{{ ansible_date_time.iso8601 }}] ROLLBACK FAILED"
|
||||
create: yes
|
||||
|
||||
- name: Fail with error message
|
||||
fail:
|
||||
msg: "Rollback failed"
|
||||
170
deployment/infrastructure/playbooks/setup-docker.yml
Normal file
170
deployment/infrastructure/playbooks/setup-docker.yml
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
# Docker Setup Playbook
|
||||
# Ensures Docker and Docker Compose are installed and configured
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventories/production/hosts.yml playbooks/setup-docker.yml
|
||||
|
||||
- name: Setup Docker for Production
|
||||
hosts: web_servers
|
||||
become: true
|
||||
|
||||
vars:
|
||||
app_user: deploy
|
||||
docker_compose_version: "2.24.0"
|
||||
|
||||
tasks:
|
||||
# ==========================================
|
||||
# 1. Verify Docker Installation
|
||||
# ==========================================
|
||||
|
||||
- name: Check if Docker is installed
|
||||
command: docker --version
|
||||
register: docker_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Display Docker version
|
||||
debug:
|
||||
msg: "Docker is already installed: {{ docker_check.stdout }}"
|
||||
when: docker_check.rc == 0
|
||||
|
||||
- name: Install Docker if not present
|
||||
block:
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Install prerequisites
|
||||
apt:
|
||||
name:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
state: present
|
||||
|
||||
- name: Add Docker GPG key
|
||||
apt_key:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
state: present
|
||||
|
||||
- name: Add Docker repository
|
||||
apt_repository:
|
||||
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
state: present
|
||||
|
||||
- name: Install Docker
|
||||
apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
state: present
|
||||
update_cache: yes
|
||||
when: docker_check.rc != 0
|
||||
|
||||
# ==========================================
|
||||
# 2. Configure Docker
|
||||
# ==========================================
|
||||
|
||||
- name: Add deploy user to docker group
|
||||
user:
|
||||
name: "{{ app_user }}"
|
||||
groups: docker
|
||||
append: yes
|
||||
|
||||
- name: Ensure Docker service is enabled and started
|
||||
systemd:
|
||||
name: docker
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
# ==========================================
|
||||
# 3. Install Docker Compose Plugin
|
||||
# ==========================================
|
||||
|
||||
- name: Check if Docker Compose plugin is installed
|
||||
command: docker compose version
|
||||
register: compose_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Display Docker Compose version
|
||||
debug:
|
||||
msg: "Docker Compose is already installed: {{ compose_check.stdout }}"
|
||||
when: compose_check.rc == 0
|
||||
|
||||
# ==========================================
|
||||
# 4. Configure Docker Daemon
|
||||
# ==========================================
|
||||
|
||||
- name: Create Docker daemon configuration
|
||||
copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
content: |
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"live-restore": true
|
||||
}
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Restart Docker
|
||||
|
||||
# ==========================================
|
||||
# 5. Firewall Configuration
|
||||
# ==========================================
|
||||
|
||||
- name: Allow HTTP traffic
|
||||
ufw:
|
||||
rule: allow
|
||||
port: '80'
|
||||
proto: tcp
|
||||
|
||||
- name: Allow HTTPS traffic
|
||||
ufw:
|
||||
rule: allow
|
||||
port: '443'
|
||||
proto: tcp
|
||||
|
||||
# ==========================================
|
||||
# 6. Verification
|
||||
# ==========================================
|
||||
|
||||
- name: Get Docker info
|
||||
command: docker info
|
||||
register: docker_info
|
||||
changed_when: false
|
||||
|
||||
- name: Get Docker Compose version
|
||||
command: docker compose version
|
||||
register: compose_version
|
||||
changed_when: false
|
||||
|
||||
- name: Display setup summary
|
||||
debug:
|
||||
msg:
|
||||
- "=========================================="
|
||||
- "Docker Setup Complete"
|
||||
- "=========================================="
|
||||
- "Docker Version: {{ docker_check.stdout }}"
|
||||
- "Docker Compose: {{ compose_version.stdout }}"
|
||||
- "User '{{ app_user }}' added to docker group"
|
||||
- "Firewall: HTTP (80) and HTTPS (443) allowed"
|
||||
- "=========================================="
|
||||
- ""
|
||||
- "Next Steps:"
|
||||
- "1. Log out and back in for docker group to take effect"
|
||||
- "2. Run deployment playbook to start containers"
|
||||
|
||||
handlers:
|
||||
- name: Restart Docker
|
||||
systemd:
|
||||
name: docker
|
||||
state: restarted
|
||||
Reference in New Issue
Block a user