feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
---
|
||||
# Git-Based Production Deployment Playbook
|
||||
# Uses Git to sync files, builds image, and updates services
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/deploy-complete-git.yml
|
||||
|
||||
- name: Git-Based Production Deployment
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
# Calculate project root: playbook is in deployment/ansible/playbooks/, go up 3 levels
|
||||
local_project_path: "{{ playbook_dir }}/../../.."
|
||||
remote_project_path: /home/deploy/framework-app
|
||||
docker_registry: localhost:5000
|
||||
docker_image_name: framework
|
||||
docker_image_tag: latest
|
||||
docker_stack_name: framework
|
||||
build_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
|
||||
tasks:
|
||||
- name: Display deployment information
|
||||
debug:
|
||||
msg:
|
||||
- "🚀 Starting Git-Based Deployment"
|
||||
- "Local Path: {{ local_project_path }}"
|
||||
- "Remote Path: {{ remote_project_path }}"
|
||||
- "Image: {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
- "Timestamp: {{ build_timestamp }}"
|
||||
|
||||
- name: Create remote project directory
|
||||
file:
|
||||
path: "{{ remote_project_path }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Check if Git repository exists on production
|
||||
stat:
|
||||
path: "{{ remote_project_path }}/.git"
|
||||
register: git_repo
|
||||
|
||||
- name: Initialize Git repository if not exists
|
||||
shell: |
|
||||
cd {{ remote_project_path }}
|
||||
git init
|
||||
git config user.email 'deploy@michaelschiemer.de'
|
||||
git config user.name 'Deploy User'
|
||||
when: not git_repo.stat.exists
|
||||
|
||||
- name: Create tarball of current code (excluding unnecessary files)
|
||||
delegate_to: localhost
|
||||
shell: |
|
||||
cd {{ local_project_path }}
|
||||
tar czf /tmp/framework-deploy-{{ build_timestamp }}.tar.gz \
|
||||
--exclude='.git' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='vendor' \
|
||||
--exclude='storage/logs/*' \
|
||||
--exclude='storage/cache/*' \
|
||||
--exclude='.env' \
|
||||
--exclude='.env.*' \
|
||||
--exclude='tests' \
|
||||
--exclude='.deployment-backup' \
|
||||
--exclude='deployment' \
|
||||
.
|
||||
register: tarball_creation
|
||||
changed_when: true
|
||||
|
||||
- name: Transfer tarball to production
|
||||
copy:
|
||||
src: "/tmp/framework-deploy-{{ build_timestamp }}.tar.gz"
|
||||
dest: "/tmp/framework-deploy-{{ build_timestamp }}.tar.gz"
|
||||
register: tarball_transfer
|
||||
|
||||
- name: Extract tarball to production (preserving Git)
|
||||
shell: |
|
||||
cd {{ remote_project_path }}
|
||||
tar xzf /tmp/framework-deploy-{{ build_timestamp }}.tar.gz
|
||||
rm -f /tmp/framework-deploy-{{ build_timestamp }}.tar.gz
|
||||
register: extraction_result
|
||||
changed_when: true
|
||||
|
||||
- name: Commit changes to Git repository
|
||||
shell: |
|
||||
cd {{ remote_project_path }}
|
||||
git add -A
|
||||
git commit -m "Deployment {{ build_timestamp }}" || echo "No changes to commit"
|
||||
git log --oneline -5
|
||||
register: git_commit
|
||||
changed_when: true
|
||||
|
||||
- name: Display Git status
|
||||
debug:
|
||||
msg: "{{ git_commit.stdout_lines }}"
|
||||
|
||||
- name: Clean up local tarball
|
||||
delegate_to: localhost
|
||||
file:
|
||||
path: "/tmp/framework-deploy-{{ build_timestamp }}.tar.gz"
|
||||
state: absent
|
||||
|
||||
- name: Build Docker image on production server
|
||||
shell: |
|
||||
cd {{ remote_project_path }}
|
||||
docker build \
|
||||
-f docker/php/Dockerfile \
|
||||
--target production \
|
||||
-t {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }} \
|
||||
-t {{ docker_registry }}/{{ docker_image_name }}:{{ build_timestamp }} \
|
||||
--no-cache \
|
||||
--progress=plain \
|
||||
.
|
||||
register: build_result
|
||||
changed_when: true
|
||||
|
||||
- name: Display build output (last 20 lines)
|
||||
debug:
|
||||
msg: "{{ build_result.stdout_lines[-20:] }}"
|
||||
|
||||
- name: Update web service with rolling update
|
||||
shell: |
|
||||
docker service update \
|
||||
--image {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }} \
|
||||
--force \
|
||||
--update-parallelism 1 \
|
||||
--update-delay 10s \
|
||||
{{ docker_stack_name }}_web
|
||||
register: web_update
|
||||
changed_when: true
|
||||
|
||||
- name: Update queue-worker service
|
||||
shell: |
|
||||
docker service update \
|
||||
--image {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }} \
|
||||
--force \
|
||||
{{ docker_stack_name }}_queue-worker
|
||||
register: worker_update
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for services to stabilize (30 seconds)
|
||||
pause:
|
||||
seconds: 30
|
||||
prompt: "Waiting for services to stabilize..."
|
||||
|
||||
- name: Check service status
|
||||
shell: docker stack services {{ docker_stack_name }} --format "table {{`{{.Name}}\t{{.Replicas}}\t{{.Image}}`}}"
|
||||
register: service_status
|
||||
changed_when: false
|
||||
|
||||
- name: Check website availability
|
||||
shell: curl -k -s -o /dev/null -w '%{http_code}' https://michaelschiemer.de/
|
||||
register: website_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Get recent web service logs
|
||||
shell: docker service logs {{ docker_stack_name }}_web --tail 10 --no-trunc 2>&1 | tail -20
|
||||
register: web_logs
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "✅ Git-Based Deployment Completed"
|
||||
- ""
|
||||
- "Build Timestamp: {{ build_timestamp }}"
|
||||
- "Image: {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
- ""
|
||||
- "Git Commit Info:"
|
||||
- "{{ git_commit.stdout_lines }}"
|
||||
- ""
|
||||
- "Service Status:"
|
||||
- "{{ service_status.stdout_lines }}"
|
||||
- ""
|
||||
- "Website HTTP Status: {{ website_check.stdout }}"
|
||||
- ""
|
||||
- "Recent Logs:"
|
||||
- "{{ web_logs.stdout_lines }}"
|
||||
- ""
|
||||
- "🌐 Website: https://michaelschiemer.de"
|
||||
- "📊 Portainer: https://michaelschiemer.de:9000"
|
||||
- "📈 Grafana: https://michaelschiemer.de:3000"
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
# Complete Production Deployment Playbook
|
||||
# Syncs files, builds image, and updates services
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/deploy-complete.yml
|
||||
|
||||
- name: Complete Production Deployment
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
# Calculate project root: playbook is in deployment/ansible/playbooks/, go up 3 levels
|
||||
local_project_path: "{{ playbook_dir }}/../../.."
|
||||
remote_project_path: /home/deploy/framework-app
|
||||
docker_registry: localhost:5000
|
||||
docker_image_name: framework
|
||||
docker_image_tag: latest
|
||||
docker_stack_name: framework
|
||||
build_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
|
||||
tasks:
|
||||
- name: Display deployment information
|
||||
debug:
|
||||
msg:
|
||||
- "🚀 Starting Complete Deployment"
|
||||
- "Local Path: {{ local_project_path }}"
|
||||
- "Remote Path: {{ remote_project_path }}"
|
||||
- "Image: {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
- "Timestamp: {{ build_timestamp }}"
|
||||
|
||||
- name: Create remote project directory
|
||||
file:
|
||||
path: "{{ remote_project_path }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Sync project files to production server
|
||||
synchronize:
|
||||
src: "{{ local_project_path }}/"
|
||||
dest: "{{ remote_project_path }}/"
|
||||
delete: no
|
||||
rsync_opts:
|
||||
- "--exclude=.git"
|
||||
- "--exclude=.gitignore"
|
||||
- "--exclude=node_modules"
|
||||
- "--exclude=vendor"
|
||||
- "--exclude=storage/logs/*"
|
||||
- "--exclude=storage/cache/*"
|
||||
- "--exclude=.env"
|
||||
- "--exclude=.env.*"
|
||||
- "--exclude=tests"
|
||||
- "--exclude=.deployment-backup"
|
||||
- "--exclude=deployment"
|
||||
register: sync_result
|
||||
|
||||
- name: Display sync results
|
||||
debug:
|
||||
msg: "Files synced: {{ sync_result.changed }}"
|
||||
|
||||
- name: Build Docker image on production server
|
||||
shell: |
|
||||
cd {{ remote_project_path }}
|
||||
docker build \
|
||||
-f Dockerfile.production \
|
||||
-t {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }} \
|
||||
-t {{ docker_registry }}/{{ docker_image_name }}:{{ build_timestamp }} \
|
||||
--no-cache \
|
||||
--progress=plain \
|
||||
.
|
||||
register: build_result
|
||||
changed_when: true
|
||||
|
||||
- name: Display build output (last 20 lines)
|
||||
debug:
|
||||
msg: "{{ build_result.stdout_lines[-20:] }}"
|
||||
|
||||
- name: Update web service with rolling update
|
||||
shell: |
|
||||
docker service update \
|
||||
--image {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }} \
|
||||
--force \
|
||||
--update-parallelism 1 \
|
||||
--update-delay 10s \
|
||||
{{ docker_stack_name }}_web
|
||||
register: web_update
|
||||
changed_when: true
|
||||
|
||||
- name: Update queue-worker service
|
||||
shell: |
|
||||
docker service update \
|
||||
--image {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }} \
|
||||
--force \
|
||||
{{ docker_stack_name }}_queue-worker
|
||||
register: worker_update
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for services to stabilize (30 seconds)
|
||||
pause:
|
||||
seconds: 30
|
||||
prompt: "Waiting for services to stabilize..."
|
||||
|
||||
- name: Check service status
|
||||
shell: docker stack services {{ docker_stack_name }} --format "table {{`{{.Name}}\t{{.Replicas}}\t{{.Image}}`}}"
|
||||
register: service_status
|
||||
changed_when: false
|
||||
|
||||
- name: Check website availability
|
||||
shell: curl -k -s -o /dev/null -w '%{http_code}' https://michaelschiemer.de/
|
||||
register: website_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Get recent web service logs
|
||||
shell: docker service logs {{ docker_stack_name }}_web --tail 10 --no-trunc 2>&1 | tail -20
|
||||
register: web_logs
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "✅ Deployment Completed"
|
||||
- ""
|
||||
- "Build Timestamp: {{ build_timestamp }}"
|
||||
- "Image: {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
- ""
|
||||
- "Service Status:"
|
||||
- "{{ service_status.stdout_lines }}"
|
||||
- ""
|
||||
- "Website HTTP Status: {{ website_check.stdout }}"
|
||||
- ""
|
||||
- "Recent Logs:"
|
||||
- "{{ web_logs.stdout_lines }}"
|
||||
- ""
|
||||
- "🌐 Website: https://michaelschiemer.de"
|
||||
- "📊 Portainer: https://michaelschiemer.de:9000"
|
||||
- "📈 Grafana: https://michaelschiemer.de:3000"
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
# Ansible Playbook: Update Production Deployment
|
||||
# Purpose: Pull new Docker image and update services with zero-downtime
|
||||
# Usage: Called by Gitea Actions or manual deployment
|
||||
|
||||
- name: Update Production Services with New Image
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
git_commit_sha: "{{ git_commit_sha | default('unknown') }}"
|
||||
deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}"
|
||||
registry_url: "git.michaelschiemer.de:5000"
|
||||
image_name: "framework"
|
||||
stack_name: "framework"
|
||||
|
||||
tasks:
|
||||
- name: Log deployment start
|
||||
debug:
|
||||
msg: |
|
||||
🚀 Starting deployment
|
||||
Image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}
|
||||
Commit: {{ git_commit_sha }}
|
||||
Time: {{ deployment_timestamp }}
|
||||
|
||||
- name: Pull new Docker image
|
||||
docker_image:
|
||||
name: "{{ registry_url }}/{{ image_name }}"
|
||||
tag: "{{ image_tag }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
register: image_pull
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: image_pull is succeeded
|
||||
|
||||
- name: Tag image as latest locally
|
||||
docker_image:
|
||||
name: "{{ registry_url }}/{{ image_name }}:{{ image_tag }}"
|
||||
repository: "{{ registry_url }}/{{ image_name }}"
|
||||
tag: latest
|
||||
source: local
|
||||
|
||||
- name: Update web service with rolling update
|
||||
docker_swarm_service:
|
||||
name: "{{ stack_name }}_web"
|
||||
image: "{{ registry_url }}/{{ image_name }}:{{ image_tag }}"
|
||||
force_update: yes
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
failure_action: rollback
|
||||
monitor: 30s
|
||||
max_failure_ratio: 0.3
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 5s
|
||||
state: present
|
||||
register: web_update
|
||||
|
||||
- name: Update queue-worker service
|
||||
docker_swarm_service:
|
||||
name: "{{ stack_name }}_queue-worker"
|
||||
image: "{{ registry_url }}/{{ image_name }}:{{ image_tag }}"
|
||||
force_update: yes
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
failure_action: rollback
|
||||
state: present
|
||||
register: worker_update
|
||||
|
||||
- name: Wait for services to stabilize
|
||||
pause:
|
||||
seconds: 20
|
||||
|
||||
- name: Verify service status
|
||||
shell: |
|
||||
docker service ps {{ stack_name }}_web --filter "desired-state=running" --format "{{`{{.CurrentState}}`}}" | head -1
|
||||
register: service_state
|
||||
changed_when: false
|
||||
|
||||
- name: Check if deployment succeeded
|
||||
fail:
|
||||
msg: "Service deployment failed: {{ service_state.stdout }}"
|
||||
when: "'Running' not in service_state.stdout"
|
||||
|
||||
- name: Get running replicas count
|
||||
shell: |
|
||||
docker service ls --filter "name={{ stack_name }}_web" --format "{{`{{.Replicas}}`}}"
|
||||
register: replicas
|
||||
changed_when: false
|
||||
|
||||
- name: Record deployment in history
|
||||
copy:
|
||||
content: |
|
||||
Deployment: {{ deployment_timestamp }}
|
||||
Image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}
|
||||
Commit: {{ git_commit_sha }}
|
||||
Status: SUCCESS
|
||||
Replicas: {{ replicas.stdout }}
|
||||
dest: "/home/deploy/deployments/{{ image_tag }}.log"
|
||||
mode: '0644'
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg: |
|
||||
✅ Deployment completed successfully
|
||||
|
||||
Image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}
|
||||
Commit: {{ git_commit_sha }}
|
||||
Web Service: {{ web_update.changed | ternary('UPDATED', 'NO CHANGE') }}
|
||||
Worker Service: {{ worker_update.changed | ternary('UPDATED', 'NO CHANGE') }}
|
||||
Replicas: {{ replicas.stdout }}
|
||||
Time: {{ deployment_timestamp }}
|
||||
|
||||
handlers:
|
||||
- name: Cleanup old images
|
||||
shell: docker image prune -af --filter "until=72h"
|
||||
changed_when: false
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
- name: Deploy Framework Application to Production
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
git_repo_url: "{{ lookup('env', 'GIT_REPO_URL') | default('') }}"
|
||||
build_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
|
||||
tasks:
|
||||
- name: Ensure git repo path exists
|
||||
file:
|
||||
path: "{{ git_repo_path }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Pull latest code from git
|
||||
git:
|
||||
repo: "{{ git_repo_url }}"
|
||||
dest: "{{ git_repo_path }}"
|
||||
version: main
|
||||
force: yes
|
||||
when: git_repo_url != ''
|
||||
register: git_pull_result
|
||||
|
||||
- name: Build Docker image on production server
|
||||
docker_image:
|
||||
name: "{{ docker_registry }}/{{ docker_image_name }}"
|
||||
tag: "{{ docker_image_tag }}"
|
||||
build:
|
||||
path: "{{ git_repo_path }}"
|
||||
dockerfile: "{{ build_dockerfile }}"
|
||||
args:
|
||||
--target: "{{ build_target }}"
|
||||
source: build
|
||||
force_source: yes
|
||||
push: no
|
||||
register: build_result
|
||||
|
||||
- name: Tag image with timestamp for rollback capability
|
||||
docker_image:
|
||||
name: "{{ docker_registry }}/{{ docker_image_name }}"
|
||||
repository: "{{ docker_registry }}/{{ docker_image_name }}"
|
||||
tag: "{{ build_timestamp }}"
|
||||
source: local
|
||||
|
||||
- name: Update Docker Swarm service - web
|
||||
docker_swarm_service:
|
||||
name: "{{ docker_swarm_stack_name }}_web"
|
||||
image: "{{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
force_update: yes
|
||||
state: present
|
||||
register: web_update_result
|
||||
|
||||
- name: Update Docker Swarm service - queue-worker
|
||||
docker_swarm_service:
|
||||
name: "{{ docker_swarm_stack_name }}_queue-worker"
|
||||
image: "{{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
force_update: yes
|
||||
state: present
|
||||
register: worker_update_result
|
||||
|
||||
- name: Wait for services to stabilize
|
||||
pause:
|
||||
seconds: 60
|
||||
|
||||
- name: Check service status
|
||||
shell: docker stack services {{ docker_swarm_stack_name }} | grep -E "NAME|{{ docker_swarm_stack_name }}"
|
||||
register: service_status
|
||||
changed_when: false
|
||||
|
||||
- name: Display deployment results
|
||||
debug:
|
||||
msg:
|
||||
- "Deployment completed successfully"
|
||||
- "Build timestamp: {{ build_timestamp }}"
|
||||
- "Image: {{ docker_registry }}/{{ docker_image_name }}:{{ docker_image_tag }}"
|
||||
- "Services status: {{ service_status.stdout_lines }}"
|
||||
|
||||
- name: Test website availability
|
||||
uri:
|
||||
url: "https://michaelschiemer.de/"
|
||||
validate_certs: no
|
||||
status_code: [200, 302]
|
||||
timeout: 10
|
||||
register: website_health
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display website health check
|
||||
debug:
|
||||
msg: "Website responded with status: {{ website_health.status | default('FAILED') }}"
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
# Ansible Playbook: Emergency Rollback
|
||||
# Purpose: Fast rollback without health checks for emergency situations
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/emergency-rollback.yml -e "rollback_tag=<tag>"
|
||||
|
||||
- name: Emergency Rollback (Fast Mode)
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
registry_url: "git.michaelschiemer.de:5000"
|
||||
image_name: "framework"
|
||||
stack_name: "framework"
|
||||
rollback_tag: "{{ rollback_tag | default('latest') }}"
|
||||
skip_health_check: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Emergency rollback warning
|
||||
debug:
|
||||
msg: |
|
||||
🚨 EMERGENCY ROLLBACK IN PROGRESS 🚨
|
||||
|
||||
This will immediately revert to: {{ rollback_tag }}
|
||||
Health checks will be SKIPPED for speed.
|
||||
|
||||
Press Ctrl+C now if you want to abort.
|
||||
|
||||
- name: Record rollback initiation
|
||||
shell: |
|
||||
echo "[$(date)] Emergency rollback initiated to {{ rollback_tag }}" >> /home/deploy/deployments/emergency-rollback.log
|
||||
|
||||
tasks:
|
||||
- name: Get current running image tag
|
||||
shell: |
|
||||
docker service inspect {{ stack_name }}_web --format '{{`{{.Spec.TaskTemplate.ContainerSpec.Image}}`}}'
|
||||
register: current_image
|
||||
changed_when: false
|
||||
|
||||
- name: Display current vs target
|
||||
debug:
|
||||
msg: |
|
||||
Current: {{ current_image.stdout }}
|
||||
Target: {{ registry_url }}/{{ image_name }}:{{ rollback_tag }}
|
||||
|
||||
- name: Pull rollback image (skip verification)
|
||||
docker_image:
|
||||
name: "{{ registry_url }}/{{ image_name }}"
|
||||
tag: "{{ rollback_tag }}"
|
||||
source: pull
|
||||
register: rollback_image
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Force rollback even if image pull failed
|
||||
debug:
|
||||
msg: "⚠️ Image pull failed, attempting rollback with cached image"
|
||||
when: rollback_image is failed
|
||||
|
||||
- name: Immediate rollback - web service
|
||||
shell: |
|
||||
docker service update \
|
||||
--image {{ registry_url }}/{{ image_name }}:{{ rollback_tag }} \
|
||||
--force \
|
||||
--update-parallelism 999 \
|
||||
--update-delay 0s \
|
||||
{{ stack_name }}_web
|
||||
register: web_rollback
|
||||
|
||||
- name: Immediate rollback - queue-worker service
|
||||
shell: |
|
||||
docker service update \
|
||||
--image {{ registry_url }}/{{ image_name }}:{{ rollback_tag }} \
|
||||
--force \
|
||||
--update-parallelism 999 \
|
||||
--update-delay 0s \
|
||||
{{ stack_name }}_queue-worker
|
||||
register: worker_rollback
|
||||
|
||||
- name: Wait for rollback to propagate (minimal wait)
|
||||
pause:
|
||||
seconds: 15
|
||||
|
||||
- name: Quick service status check
|
||||
shell: |
|
||||
docker service ps {{ stack_name }}_web --filter "desired-state=running" --format "{{`{{.CurrentState}}`}}" | head -1
|
||||
register: rollback_state
|
||||
changed_when: false
|
||||
|
||||
- name: Display rollback status
|
||||
debug:
|
||||
msg: |
|
||||
🚨 Emergency rollback completed (fast mode)
|
||||
|
||||
Web Service: {{ web_rollback.changed | ternary('ROLLED BACK', 'NO CHANGE') }}
|
||||
Worker Service: {{ worker_rollback.changed | ternary('ROLLED BACK', 'NO CHANGE') }}
|
||||
Service State: {{ rollback_state.stdout }}
|
||||
|
||||
⚠️ MANUAL VERIFICATION REQUIRED:
|
||||
1. Check application: https://michaelschiemer.de
|
||||
2. Check service logs: docker service logs {{ stack_name }}_web
|
||||
3. Verify database connectivity
|
||||
4. Run full health check: ansible-playbook playbooks/health-check.yml
|
||||
|
||||
- name: Record rollback completion
|
||||
shell: |
|
||||
echo "[$(date)] Emergency rollback completed: {{ rollback_tag }}, Status: {{ rollback_state.stdout }}" >> /home/deploy/deployments/emergency-rollback.log
|
||||
|
||||
- name: Alert - manual verification required
|
||||
debug:
|
||||
msg: |
|
||||
⚠️ IMPORTANT: This was an emergency rollback without health checks.
|
||||
You MUST manually verify application functionality before considering this successful.
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
# Ansible Playbook: Production Health Check
|
||||
# Purpose: Comprehensive health verification for production deployment
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/health-check.yml
|
||||
|
||||
- name: Production Health Check
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
app_url: "https://michaelschiemer.de"
|
||||
stack_name: "framework"
|
||||
health_timeout: 30
|
||||
max_retries: 10
|
||||
|
||||
tasks:
|
||||
- name: Check Docker Swarm status
|
||||
shell: docker info | grep "Swarm: active"
|
||||
register: swarm_status
|
||||
failed_when: swarm_status.rc != 0
|
||||
changed_when: false
|
||||
|
||||
- name: Check running services
|
||||
shell: docker service ls --filter "name={{ stack_name }}" --format "{{`{{.Name}}`}} {{`{{.Replicas}}`}}"
|
||||
register: service_list
|
||||
changed_when: false
|
||||
|
||||
- name: Display service status
|
||||
debug:
|
||||
msg: "{{ service_list.stdout_lines }}"
|
||||
|
||||
- name: Verify web service is running
|
||||
shell: |
|
||||
docker service ps {{ stack_name }}_web \
|
||||
--filter "desired-state=running" \
|
||||
--format "{{`{{.CurrentState}}`}}" | head -1
|
||||
register: web_state
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if web service not running
|
||||
fail:
|
||||
msg: "Web service is not in Running state: {{ web_state.stdout }}"
|
||||
when: "'Running' not in web_state.stdout"
|
||||
|
||||
- name: Verify worker service is running
|
||||
shell: |
|
||||
docker service ps {{ stack_name }}_queue-worker \
|
||||
--filter "desired-state=running" \
|
||||
--format "{{`{{.CurrentState}}`}}" | head -1
|
||||
register: worker_state
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if worker service not running
|
||||
fail:
|
||||
msg: "Worker service is not in Running state: {{ worker_state.stdout }}"
|
||||
when: "'Running' not in worker_state.stdout"
|
||||
|
||||
- name: Wait for application to be ready
|
||||
uri:
|
||||
url: "{{ app_url }}/health"
|
||||
validate_certs: no
|
||||
status_code: [200, 302]
|
||||
timeout: "{{ health_timeout }}"
|
||||
register: health_response
|
||||
retries: "{{ max_retries }}"
|
||||
delay: 3
|
||||
until: health_response.status in [200, 302]
|
||||
|
||||
- name: Check database connectivity
|
||||
uri:
|
||||
url: "{{ app_url }}/health/database"
|
||||
validate_certs: no
|
||||
status_code: 200
|
||||
timeout: "{{ health_timeout }}"
|
||||
register: db_health
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Check Redis connectivity
|
||||
uri:
|
||||
url: "{{ app_url }}/health/redis"
|
||||
validate_certs: no
|
||||
status_code: 200
|
||||
timeout: "{{ health_timeout }}"
|
||||
register: redis_health
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Check queue system
|
||||
uri:
|
||||
url: "{{ app_url }}/health/queue"
|
||||
validate_certs: no
|
||||
status_code: 200
|
||||
timeout: "{{ health_timeout }}"
|
||||
register: queue_health
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Get service replicas count
|
||||
shell: |
|
||||
docker service ls --filter "name={{ stack_name }}_web" --format "{{`{{.Replicas}}`}}"
|
||||
register: replicas
|
||||
changed_when: false
|
||||
|
||||
- name: Check for service errors
|
||||
shell: |
|
||||
docker service ps {{ stack_name }}_web --filter "desired-state=running" | grep -c Error || true
|
||||
register: error_count
|
||||
changed_when: false
|
||||
|
||||
- name: Warn if errors detected
|
||||
debug:
|
||||
msg: "⚠️ Warning: {{ error_count.stdout }} errors detected in service logs"
|
||||
when: error_count.stdout | int > 0
|
||||
|
||||
- name: Display health check summary
|
||||
debug:
|
||||
msg: |
|
||||
✅ Health Check Summary:
|
||||
|
||||
Services:
|
||||
- Web Service: {{ web_state.stdout }}
|
||||
- Worker Service: {{ worker_state.stdout }}
|
||||
- Replicas: {{ replicas.stdout }}
|
||||
|
||||
Endpoints:
|
||||
- Application: {{ health_response.status }}
|
||||
- Database: {{ db_health.status | default('SKIPPED') }}
|
||||
- Redis: {{ redis_health.status | default('SKIPPED') }}
|
||||
- Queue: {{ queue_health.status | default('SKIPPED') }}
|
||||
|
||||
Errors: {{ error_count.stdout }}
|
||||
|
||||
- name: Overall health assessment
|
||||
debug:
|
||||
msg: "✅ All health checks PASSED"
|
||||
when:
|
||||
- health_response.status in [200, 302]
|
||||
- error_count.stdout | int == 0
|
||||
|
||||
- name: Fail if critical health checks failed
|
||||
fail:
|
||||
msg: "❌ Health check FAILED - manual intervention required"
|
||||
when: health_response.status not in [200, 302]
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
# Ansible Playbook: Emergency Rollback
|
||||
# Purpose: Rollback to previous working deployment
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/rollback.yml
|
||||
|
||||
- name: Rollback Production Deployment
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars:
|
||||
registry_url: "git.michaelschiemer.de:5000"
|
||||
image_name: "framework"
|
||||
stack_name: "framework"
|
||||
rollback_tag: "{{ rollback_tag | default('latest') }}"
|
||||
|
||||
tasks:
|
||||
- name: Display rollback warning
|
||||
debug:
|
||||
msg: |
|
||||
⚠️ ROLLBACK IN PROGRESS
|
||||
|
||||
This will revert services to a previous image.
|
||||
Current target: {{ rollback_tag }}
|
||||
|
||||
- name: Pause for confirmation (manual runs only)
|
||||
pause:
|
||||
prompt: "Press ENTER to continue with rollback, or Ctrl+C to abort"
|
||||
when: ansible_check_mode is not defined
|
||||
|
||||
- name: Get list of available image tags
|
||||
shell: |
|
||||
docker images {{ registry_url }}/{{ image_name }} --format "{{`{{.Tag}}`}}" | grep -v buildcache | head -10
|
||||
register: available_tags
|
||||
changed_when: false
|
||||
|
||||
- name: Display available tags
|
||||
debug:
|
||||
msg: |
|
||||
Available image tags for rollback:
|
||||
{{ available_tags.stdout_lines | join('\n') }}
|
||||
|
||||
- name: Verify rollback image exists
|
||||
docker_image:
|
||||
name: "{{ registry_url }}/{{ image_name }}"
|
||||
tag: "{{ rollback_tag }}"
|
||||
source: pull
|
||||
register: rollback_image
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Fail if image doesn't exist
|
||||
fail:
|
||||
msg: "Rollback image {{ registry_url }}/{{ image_name }}:{{ rollback_tag }} not found"
|
||||
when: rollback_image is failed
|
||||
|
||||
- name: Rollback web service
|
||||
docker_swarm_service:
|
||||
name: "{{ stack_name }}_web"
|
||||
image: "{{ registry_url }}/{{ image_name }}:{{ rollback_tag }}"
|
||||
force_update: yes
|
||||
update_config:
|
||||
parallelism: 2
|
||||
delay: 5s
|
||||
state: present
|
||||
register: web_rollback
|
||||
|
||||
- name: Rollback queue-worker service
|
||||
docker_swarm_service:
|
||||
name: "{{ stack_name }}_queue-worker"
|
||||
image: "{{ registry_url }}/{{ image_name }}:{{ rollback_tag }}"
|
||||
force_update: yes
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 5s
|
||||
state: present
|
||||
register: worker_rollback
|
||||
|
||||
- name: Wait for rollback to complete
|
||||
pause:
|
||||
seconds: 30
|
||||
|
||||
- name: Verify rollback success
|
||||
shell: |
|
||||
docker service ps {{ stack_name }}_web --filter "desired-state=running" --format "{{`{{.CurrentState}}`}}" | head -1
|
||||
register: rollback_state
|
||||
changed_when: false
|
||||
|
||||
- name: Test service health
|
||||
uri:
|
||||
url: "https://michaelschiemer.de/health"
|
||||
validate_certs: no
|
||||
status_code: [200, 302]
|
||||
timeout: 10
|
||||
register: health_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Record rollback in history
|
||||
copy:
|
||||
content: |
|
||||
Rollback: {{ ansible_date_time.iso8601 }}
|
||||
Previous Image: {{ registry_url }}/{{ image_name }}:latest
|
||||
Rollback Image: {{ registry_url }}/{{ image_name }}:{{ rollback_tag }}
|
||||
Status: {{ health_check.status | default('UNKNOWN') }}
|
||||
Reason: Manual rollback or deployment failure
|
||||
dest: "/home/deploy/deployments/rollback-{{ ansible_date_time.epoch }}.log"
|
||||
mode: '0644'
|
||||
|
||||
- name: Display rollback summary
|
||||
debug:
|
||||
msg: |
|
||||
{% if health_check is succeeded %}
|
||||
✅ Rollback completed successfully
|
||||
{% else %}
|
||||
❌ Rollback completed but health check failed
|
||||
{% endif %}
|
||||
|
||||
Image: {{ registry_url }}/{{ image_name }}:{{ rollback_tag }}
|
||||
Web Service: {{ web_rollback.changed | ternary('ROLLED BACK', 'NO CHANGE') }}
|
||||
Worker Service: {{ worker_rollback.changed | ternary('ROLLED BACK', 'NO CHANGE') }}
|
||||
Health Status: {{ health_check.status | default('FAILED') }}
|
||||
|
||||
- name: Alert if rollback failed
|
||||
fail:
|
||||
msg: "Rollback completed but health check failed. Manual intervention required."
|
||||
when: health_check is failed
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
# Ansible Playbook: Setup Gitea Actions Runner on Production Server
|
||||
# Purpose: Install and configure Gitea Actions runner for automated deployments
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/setup-gitea-runner.yml
|
||||
|
||||
- name: Setup Gitea Actions Runner for Production Deployments
|
||||
hosts: production_server
|
||||
become: yes
|
||||
vars:
|
||||
gitea_url: "https://git.michaelschiemer.de"
|
||||
runner_name: "production-runner"
|
||||
runner_labels: "docker,production,ubuntu"
|
||||
runner_version: "0.2.6"
|
||||
runner_install_dir: "/opt/gitea-runner"
|
||||
runner_work_dir: "/home/deploy/gitea-runner-work"
|
||||
runner_user: "deploy"
|
||||
|
||||
tasks:
|
||||
- name: Create runner directories
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ runner_user }}"
|
||||
group: "{{ runner_user }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- "{{ runner_install_dir }}"
|
||||
- "{{ runner_work_dir }}"
|
||||
|
||||
- name: Download Gitea Act Runner binary
|
||||
get_url:
|
||||
url: "https://dl.gitea.com/act_runner/{{ runner_version }}/act_runner-{{ runner_version }}-linux-amd64"
|
||||
dest: "{{ runner_install_dir }}/act_runner"
|
||||
mode: '0755'
|
||||
owner: "{{ runner_user }}"
|
||||
|
||||
- name: Check if runner is already registered
|
||||
stat:
|
||||
path: "{{ runner_install_dir }}/.runner"
|
||||
register: runner_config
|
||||
|
||||
- name: Register runner with Gitea (manual step required)
|
||||
debug:
|
||||
msg: |
|
||||
⚠️ MANUAL STEP REQUIRED:
|
||||
|
||||
1. Generate registration token in Gitea:
|
||||
- Navigate to {{ gitea_url }}/admin/runners
|
||||
- Click "Create new runner"
|
||||
- Copy the registration token
|
||||
|
||||
2. SSH to production server and run:
|
||||
sudo -u {{ runner_user }} {{ runner_install_dir }}/act_runner register \
|
||||
--instance {{ gitea_url }} \
|
||||
--token YOUR_REGISTRATION_TOKEN \
|
||||
--name {{ runner_name }} \
|
||||
--labels {{ runner_labels }}
|
||||
|
||||
3. Re-run this playbook to complete setup
|
||||
when: not runner_config.stat.exists
|
||||
|
||||
- name: Create systemd service for runner
|
||||
template:
|
||||
src: ../templates/gitea-runner.service.j2
|
||||
dest: /etc/systemd/system/gitea-runner.service
|
||||
mode: '0644'
|
||||
notify: Reload systemd
|
||||
|
||||
- name: Enable and start Gitea runner service
|
||||
systemd:
|
||||
name: gitea-runner
|
||||
enabled: yes
|
||||
state: started
|
||||
when: runner_config.stat.exists
|
||||
|
||||
- name: Install Docker (if not present)
|
||||
apt:
|
||||
name:
|
||||
- docker.io
|
||||
- docker-compose
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Add runner user to docker group
|
||||
user:
|
||||
name: "{{ runner_user }}"
|
||||
groups: docker
|
||||
append: yes
|
||||
|
||||
- name: Ensure Docker service is running
|
||||
systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: Create Docker network for builds
|
||||
docker_network:
|
||||
name: gitea-runner-network
|
||||
driver: bridge
|
||||
|
||||
- name: Display runner status
|
||||
debug:
|
||||
msg: |
|
||||
✅ Gitea Runner Setup Complete
|
||||
|
||||
Runner Name: {{ runner_name }}
|
||||
Install Dir: {{ runner_install_dir }}
|
||||
Work Dir: {{ runner_work_dir }}
|
||||
|
||||
Check status: systemctl status gitea-runner
|
||||
View logs: journalctl -u gitea-runner -f
|
||||
|
||||
handlers:
|
||||
- name: Reload systemd
|
||||
systemd:
|
||||
daemon_reload: yes
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
# Ansible Playbook: Setup Production Secrets
|
||||
# Purpose: Deploy Docker Secrets and environment configuration to production
|
||||
# Usage: ansible-playbook -i inventory/production.yml playbooks/setup-production-secrets.yml --ask-vault-pass
|
||||
|
||||
- name: Setup Production Secrets and Environment
|
||||
hosts: production_server
|
||||
become: no
|
||||
vars_files:
|
||||
- ../secrets/production-vault.yml # Encrypted with ansible-vault
|
||||
|
||||
tasks:
|
||||
- name: Ensure secrets directory exists
|
||||
file:
|
||||
path: /home/deploy/secrets
|
||||
state: directory
|
||||
mode: '0700'
|
||||
owner: deploy
|
||||
group: deploy
|
||||
|
||||
- name: Deploy environment file from vault
|
||||
template:
|
||||
src: ../templates/production.env.j2
|
||||
dest: /home/deploy/secrets/.env.production
|
||||
mode: '0600'
|
||||
owner: deploy
|
||||
group: deploy
|
||||
notify: Restart services
|
||||
|
||||
- name: Create Docker secrets (if swarm is initialized)
|
||||
docker_secret:
|
||||
name: "{{ item.name }}"
|
||||
data: "{{ item.value }}"
|
||||
state: present
|
||||
loop:
|
||||
- { name: "db_password", value: "{{ vault_db_password }}" }
|
||||
- { name: "redis_password", value: "{{ vault_redis_password }}" }
|
||||
- { name: "app_key", value: "{{ vault_app_key }}" }
|
||||
- { name: "jwt_secret", value: "{{ vault_jwt_secret }}" }
|
||||
- { name: "registry_password", value: "{{ vault_registry_password }}" }
|
||||
no_log: true # Don't log secrets
|
||||
|
||||
- name: Verify secrets are accessible
|
||||
shell: docker secret ls
|
||||
register: secret_list
|
||||
changed_when: false
|
||||
|
||||
- name: Display deployed secrets (names only)
|
||||
debug:
|
||||
msg: "Deployed secrets: {{ secret_list.stdout_lines }}"
|
||||
|
||||
handlers:
|
||||
- name: Restart services
|
||||
shell: |
|
||||
docker service update --force framework_web
|
||||
docker service update --force framework_queue-worker
|
||||
when: ansible_check_mode is not defined
|
||||
Reference in New Issue
Block a user