feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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') }}"

View File

@@ -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.

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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