feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
10
.deployment-archive-20251030-111806/ansible/ansible.cfg
Normal file
10
.deployment-archive-20251030-111806/ansible/ansible.cfg
Normal file
@@ -0,0 +1,10 @@
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
roles_path = roles
|
||||
interpreter_python = auto_silent
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=30 -o ServerAliveCountMax=3
|
||||
pipelining = True
|
||||
@@ -0,0 +1,20 @@
|
||||
all:
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
ansible_user: deploy
|
||||
ansible_ssh_private_key_file: ~/.ssh/production
|
||||
|
||||
production:
|
||||
hosts:
|
||||
production_server:
|
||||
ansible_host: 94.16.110.151
|
||||
docker_registry: localhost:5000
|
||||
docker_image_name: framework
|
||||
docker_image_tag: latest
|
||||
docker_swarm_stack_name: framework
|
||||
docker_services:
|
||||
- framework_web
|
||||
- framework_queue-worker
|
||||
git_repo_path: /home/deploy/framework-app
|
||||
build_dockerfile: Dockerfile.production
|
||||
build_target: production
|
||||
@@ -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
|
||||
8
.deployment-archive-20251030-111806/ansible/secrets/.gitignore
vendored
Normal file
8
.deployment-archive-20251030-111806/ansible/secrets/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# SECURITY: Never commit decrypted vault files
|
||||
production-vault.yml.decrypted
|
||||
*.backup
|
||||
*.tmp
|
||||
|
||||
# Keep encrypted vault in git
|
||||
# Encrypted files are safe to commit
|
||||
!production-vault.yml
|
||||
238
.deployment-archive-20251030-111806/ansible/secrets/README.md
Normal file
238
.deployment-archive-20251030-111806/ansible/secrets/README.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Production Secrets Management
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains encrypted production secrets managed with Ansible Vault.
|
||||
|
||||
**Security Model**:
|
||||
- Secrets are encrypted at rest with AES256
|
||||
- Vault password is required for deployment
|
||||
- Decrypted files are NEVER committed to git
|
||||
- Production deployment uses secure SSH key authentication
|
||||
|
||||
## Files
|
||||
|
||||
- `production-vault.yml` - **Encrypted** secrets vault (safe to commit)
|
||||
- `.gitignore` - Prevents accidental commit of decrypted files
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Initialize Secrets (First Time)
|
||||
|
||||
```bash
|
||||
cd deployment
|
||||
./scripts/setup-production-secrets.sh init
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate secure random passwords/keys
|
||||
- Create encrypted vault file
|
||||
- Prompt for vault password (store in password manager!)
|
||||
|
||||
### 2. Deploy Secrets to Production
|
||||
|
||||
```bash
|
||||
./scripts/setup-production-secrets.sh deploy
|
||||
```
|
||||
|
||||
Or via Gitea Actions:
|
||||
1. Go to: https://git.michaelschiemer.de/michael/framework/actions
|
||||
2. Select "Update Production Secrets" workflow
|
||||
3. Click "Run workflow"
|
||||
4. Enter vault password
|
||||
5. Click "Run"
|
||||
|
||||
### 3. Update Secrets Manually
|
||||
|
||||
```bash
|
||||
# Edit encrypted vault
|
||||
ansible-vault edit deployment/ansible/secrets/production-vault.yml
|
||||
|
||||
# Deploy changes
|
||||
./scripts/setup-production-secrets.sh deploy
|
||||
```
|
||||
|
||||
### 4. Rotate Secrets (Monthly Recommended)
|
||||
|
||||
```bash
|
||||
./scripts/setup-production-secrets.sh rotate
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate new passwords
|
||||
- Update vault
|
||||
- Deploy to production
|
||||
- Restart services
|
||||
|
||||
## Vault Structure
|
||||
|
||||
```yaml
|
||||
# Database
|
||||
vault_db_name: framework_production
|
||||
vault_db_user: framework_app
|
||||
vault_db_password: [auto-generated 32 chars]
|
||||
|
||||
# Redis
|
||||
vault_redis_password: [auto-generated 32 chars]
|
||||
|
||||
# Application
|
||||
vault_app_key: [auto-generated base64 key]
|
||||
vault_jwt_secret: [auto-generated 64 chars]
|
||||
|
||||
# Docker Registry
|
||||
vault_registry_url: git.michaelschiemer.de:5000
|
||||
vault_registry_user: deploy
|
||||
vault_registry_password: [auto-generated 24 chars]
|
||||
|
||||
# Security
|
||||
vault_admin_allowed_ips: "127.0.0.1,::1,94.16.110.151"
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### DO ✅
|
||||
|
||||
- **DO** encrypt vault with strong password
|
||||
- **DO** store vault password in password manager
|
||||
- **DO** rotate secrets monthly
|
||||
- **DO** use `--ask-vault-pass` for deployments
|
||||
- **DO** commit encrypted vault to git
|
||||
- **DO** use different vault passwords per environment
|
||||
|
||||
### DON'T ❌
|
||||
|
||||
- **DON'T** commit decrypted vault files
|
||||
- **DON'T** share vault password via email/chat
|
||||
- **DON'T** use weak vault passwords
|
||||
- **DON'T** decrypt vault on untrusted systems
|
||||
- **DON'T** hardcode secrets in code
|
||||
|
||||
## Ansible Vault Commands
|
||||
|
||||
```bash
|
||||
# Encrypt file
|
||||
ansible-vault encrypt production-vault.yml
|
||||
|
||||
# Decrypt file (for viewing only)
|
||||
ansible-vault decrypt production-vault.yml
|
||||
|
||||
# Edit encrypted file
|
||||
ansible-vault edit production-vault.yml
|
||||
|
||||
# Change vault password
|
||||
ansible-vault rekey production-vault.yml
|
||||
|
||||
# View encrypted file content
|
||||
ansible-vault view production-vault.yml
|
||||
```
|
||||
|
||||
## Deployment Integration
|
||||
|
||||
### Local Deployment
|
||||
|
||||
```bash
|
||||
cd deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/setup-production-secrets.yml \
|
||||
--ask-vault-pass
|
||||
```
|
||||
|
||||
### CI/CD Deployment (Gitea Actions)
|
||||
|
||||
Vault password stored as Gitea Secret:
|
||||
- Secret name: `ANSIBLE_VAULT_PASSWORD`
|
||||
- Used in workflow: `.gitea/workflows/update-production-secrets.yml`
|
||||
|
||||
### Docker Secrets Integration
|
||||
|
||||
Secrets are deployed as Docker Secrets for secure runtime access:
|
||||
|
||||
```bash
|
||||
# List deployed secrets on production
|
||||
ssh deploy@94.16.110.151 "docker secret ls"
|
||||
|
||||
# Services automatically use secrets via docker-compose
|
||||
services:
|
||||
web:
|
||||
secrets:
|
||||
- db_password
|
||||
- redis_password
|
||||
- app_key
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Decryption failed" Error
|
||||
|
||||
**Cause**: Wrong vault password
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Verify password works
|
||||
ansible-vault view deployment/ansible/secrets/production-vault.yml
|
||||
|
||||
# If forgotten, you must reinitialize (data loss!)
|
||||
./scripts/setup-production-secrets.sh init
|
||||
```
|
||||
|
||||
### Secrets Not Applied After Deployment
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Manually restart services
|
||||
ssh deploy@94.16.110.151 "docker service update --force framework_web"
|
||||
|
||||
# Or use Ansible
|
||||
cd deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml playbooks/restart-services.yml
|
||||
```
|
||||
|
||||
### Verify Secrets on Production
|
||||
|
||||
```bash
|
||||
./scripts/setup-production-secrets.sh verify
|
||||
|
||||
# Or manually
|
||||
ssh deploy@94.16.110.151 "docker secret ls"
|
||||
ssh deploy@94.16.110.151 "cat /home/deploy/secrets/.env.production | grep -v PASSWORD"
|
||||
```
|
||||
|
||||
## Emergency Procedures
|
||||
|
||||
### Lost Vault Password
|
||||
|
||||
**Recovery Steps**:
|
||||
1. Backup current vault: `cp production-vault.yml production-vault.yml.lost`
|
||||
2. Reinitialize vault: `./scripts/setup-production-secrets.sh init`
|
||||
3. Update database passwords manually on production
|
||||
4. Deploy new secrets: `./scripts/setup-production-secrets.sh deploy`
|
||||
|
||||
### Compromised Secrets
|
||||
|
||||
**Immediate Response**:
|
||||
1. Rotate all secrets: `./scripts/setup-production-secrets.sh rotate`
|
||||
2. Review access logs on production
|
||||
3. Update vault password: `ansible-vault rekey production-vault.yml`
|
||||
4. Audit git commit history
|
||||
5. Investigate compromise source
|
||||
|
||||
## Monitoring
|
||||
|
||||
Check secrets deployment status:
|
||||
|
||||
```bash
|
||||
# Via script
|
||||
./scripts/setup-production-secrets.sh verify
|
||||
|
||||
# Manual check
|
||||
ansible production_server -i inventory/production.yml \
|
||||
-m shell -a "docker secret ls | wc -l"
|
||||
|
||||
# Should show 5 secrets: db_password, redis_password, app_key, jwt_secret, registry_password
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Ansible Vault Documentation](https://docs.ansible.com/ansible/latest/user_guide/vault.html)
|
||||
- [Docker Secrets Best Practices](https://docs.docker.com/engine/swarm/secrets/)
|
||||
- Main Deployment Guide: `../README.md`
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
# Production Secrets Vault
|
||||
# IMPORTANT: This file must be encrypted with ansible-vault
|
||||
#
|
||||
# Encrypt this file:
|
||||
# ansible-vault encrypt deployment/ansible/secrets/production-vault.yml
|
||||
#
|
||||
# Edit encrypted file:
|
||||
# ansible-vault edit deployment/ansible/secrets/production-vault.yml
|
||||
#
|
||||
# Decrypt file (for debugging only, never commit decrypted):
|
||||
# ansible-vault decrypt deployment/ansible/secrets/production-vault.yml
|
||||
#
|
||||
# Use in playbook:
|
||||
# ansible-playbook playbooks/setup-production-secrets.yml --ask-vault-pass
|
||||
|
||||
# Database Credentials
|
||||
vault_db_name: framework_production
|
||||
vault_db_user: framework_app
|
||||
vault_db_password: CHANGE_ME_STRONG_DB_PASSWORD_HERE
|
||||
|
||||
# Redis Credentials
|
||||
vault_redis_password: CHANGE_ME_STRONG_REDIS_PASSWORD_HERE
|
||||
|
||||
# Application Secrets
|
||||
vault_app_key: CHANGE_ME_BASE64_ENCODED_32_BYTE_KEY
|
||||
vault_jwt_secret: CHANGE_ME_STRONG_JWT_SECRET_HERE
|
||||
|
||||
# Docker Registry Credentials
|
||||
vault_registry_url: git.michaelschiemer.de:5000
|
||||
vault_registry_user: deploy
|
||||
vault_registry_password: CHANGE_ME_REGISTRY_PASSWORD_HERE
|
||||
|
||||
# Security Configuration
|
||||
vault_admin_allowed_ips: "127.0.0.1,::1,94.16.110.151"
|
||||
|
||||
# SMTP Configuration (optional)
|
||||
vault_smtp_host: smtp.example.com
|
||||
vault_smtp_port: 587
|
||||
vault_smtp_user: noreply@michaelschiemer.de
|
||||
vault_smtp_password: CHANGE_ME_SMTP_PASSWORD_HERE
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=Gitea Actions Runner
|
||||
After=network.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ runner_user }}
|
||||
WorkingDirectory={{ runner_install_dir }}
|
||||
ExecStart={{ runner_install_dir }}/act_runner daemon --config {{ runner_install_dir }}/.runner
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths={{ runner_work_dir }}
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=4096
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,50 @@
|
||||
# Production Environment Configuration
|
||||
# Generated by Ansible - DO NOT EDIT MANUALLY
|
||||
# Last updated: {{ ansible_date_time.iso8601 }}
|
||||
|
||||
# Application
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY={{ vault_app_key }}
|
||||
APP_URL=https://michaelschiemer.de
|
||||
|
||||
# Database
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE={{ vault_db_name }}
|
||||
DB_USERNAME={{ vault_db_user }}
|
||||
DB_PASSWORD={{ vault_db_password }}
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD={{ vault_redis_password }}
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Cache
|
||||
CACHE_DRIVER=redis
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
# Session
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
# JWT
|
||||
JWT_SECRET={{ vault_jwt_secret }}
|
||||
JWT_TTL=60
|
||||
|
||||
# Docker Registry
|
||||
REGISTRY_URL={{ vault_registry_url }}
|
||||
REGISTRY_USER={{ vault_registry_user }}
|
||||
REGISTRY_PASSWORD={{ vault_registry_password }}
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=warning
|
||||
|
||||
# Security
|
||||
ADMIN_ALLOWED_IPS={{ vault_admin_allowed_ips }}
|
||||
|
||||
# Performance
|
||||
OPCACHE_ENABLE=1
|
||||
OPCACHE_VALIDATE_TIMESTAMPS=0
|
||||
Reference in New Issue
Block a user