ci: setup CI/CD pipeline with Gitea Actions and secrets configuration
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
---
|
||||
- name: Deploy Application Update
|
||||
- name: Deploy Application Update via Docker Compose
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# These should be passed via -e from CI/CD
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
git_commit_sha: "{{ git_commit_sha | default('unknown') }}"
|
||||
deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}"
|
||||
app_stack_path: "{{ deploy_user_home }}/deployment/stacks/application"
|
||||
|
||||
pre_tasks:
|
||||
- name: Optionally load registry credentials from encrypted vault
|
||||
@@ -29,12 +30,21 @@
|
||||
name: docker
|
||||
state: started
|
||||
register: docker_service
|
||||
become: yes
|
||||
|
||||
- name: Fail if Docker is not running
|
||||
fail:
|
||||
msg: "Docker service is not running"
|
||||
when: docker_service.status.ActiveState != 'active'
|
||||
|
||||
- name: Ensure application stack directory exists
|
||||
file:
|
||||
path: "{{ app_stack_path }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create backup directory
|
||||
file:
|
||||
path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}"
|
||||
@@ -46,16 +56,15 @@
|
||||
tasks:
|
||||
- name: Backup current deployment metadata
|
||||
shell: |
|
||||
docker service inspect {{ stack_name }}_app --format '{{ "{{" }}.Spec.TaskTemplate.ContainerSpec.Image{{ "}}" }}' \
|
||||
> {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_image.txt || true
|
||||
docker stack ps {{ stack_name }} --format 'table {{ "{{" }}.Name{{ "}}" }}\t{{ "{{" }}.Image{{ "}}" }}\t{{ "{{" }}.CurrentState{{ "}}" }}' \
|
||||
> {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/stack_status.txt || true
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.yml config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Login to Docker registry (if credentials provided)
|
||||
docker_login:
|
||||
community.docker.docker_login:
|
||||
registry_url: "{{ docker_registry_url }}"
|
||||
username: "{{ docker_registry_username }}"
|
||||
password: "{{ docker_registry_password }}"
|
||||
@@ -65,43 +74,55 @@
|
||||
- docker_registry_password is defined
|
||||
|
||||
- name: Pull new Docker image
|
||||
docker_image:
|
||||
community.docker.docker_image:
|
||||
name: "{{ app_image }}"
|
||||
tag: "{{ image_tag }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
register: image_pull
|
||||
|
||||
- name: Update docker-compose.prod.yml with new image tag
|
||||
lineinfile:
|
||||
path: "{{ compose_file }}"
|
||||
- name: Verify image was pulled successfully
|
||||
fail:
|
||||
msg: "Failed to pull image {{ app_image }}:{{ image_tag }}"
|
||||
when: image_pull.failed
|
||||
|
||||
- name: Update docker-compose.yml with new image tag (all services)
|
||||
replace:
|
||||
path: "{{ app_stack_path }}/docker-compose.yml"
|
||||
regexp: '^(\s+image:\s+){{ app_image }}:.*$'
|
||||
line: '\1{{ app_image }}:{{ image_tag }}'
|
||||
backrefs: yes
|
||||
when: compose_file is file
|
||||
replace: '\1{{ app_image }}:{{ image_tag }}'
|
||||
when: image_pull.changed or image_tag != 'latest'
|
||||
register: compose_updated
|
||||
|
||||
- name: Deploy stack update
|
||||
docker_stack:
|
||||
name: "{{ stack_name }}"
|
||||
compose:
|
||||
- "{{ compose_file }}"
|
||||
- name: Restart application stack with new image
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_stack_path }}"
|
||||
state: present
|
||||
prune: yes
|
||||
pull: always
|
||||
recreate: always
|
||||
remove_orphans: yes
|
||||
register: stack_deploy
|
||||
when: image_pull.changed or compose_updated.changed
|
||||
|
||||
- name: Wait for service to be updated
|
||||
command: >
|
||||
docker service ps {{ stack_name }}_app
|
||||
--filter "desired-state=running"
|
||||
--format '{{ "{{" }}.CurrentState{{ "}}" }}'
|
||||
register: service_status
|
||||
until: "'Running' in service_status.stdout"
|
||||
retries: 30
|
||||
delay: 10
|
||||
- name: Wait for services to be healthy
|
||||
wait_for:
|
||||
timeout: 60
|
||||
changed_when: false
|
||||
|
||||
- name: Get updated service info
|
||||
command: docker service inspect {{ stack_name }}_app --format '{{ "{{" }}.Spec.TaskTemplate.ContainerSpec.Image{{ "}}" }}'
|
||||
- name: Check container health status
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json | jq -r '.[] | select(.Health != "healthy" and .Health != "") | "\(.Name): \(.Health)"' || echo "All healthy or no health checks"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: health_status
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
?? - name: Get deployed image information
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: deployed_image
|
||||
changed_when: false
|
||||
|
||||
@@ -112,7 +133,9 @@
|
||||
Git Commit: {{ git_commit_sha }}
|
||||
Image Tag: {{ image_tag }}
|
||||
Deployed Image: {{ deployed_image.stdout }}
|
||||
Stack Deploy Output: {{ stack_deploy }}
|
||||
Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }}
|
||||
Stack Deploy: {{ 'UPDATED' if stack_deploy.changed else 'NO_CHANGE' }}
|
||||
Health Status: {{ health_status.stdout if health_status.stdout != '' else 'All services healthy' }}
|
||||
dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
@@ -121,17 +144,22 @@
|
||||
- name: Cleanup old backups (keep last {{ max_rollback_versions }})
|
||||
shell: |
|
||||
cd {{ backups_path }}
|
||||
ls -t | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf
|
||||
ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
post_tasks:
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "Deployment completed successfully!"
|
||||
- "=== Deployment Summary ==="
|
||||
- "Image: {{ app_image }}:{{ image_tag }}"
|
||||
- "Commit: {{ git_commit_sha }}"
|
||||
- "Timestamp: {{ deployment_timestamp }}"
|
||||
- "Health check URL: {{ health_check_url }}"
|
||||
- "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}"
|
||||
- "Stack Deploy: {{ 'UPDATED' if stack_deploy.changed else 'NO_CHANGE' }}"
|
||||
- "Health Check URL: {{ health_check_url }}"
|
||||
- ""
|
||||
- "Next: Verify application is healthy"
|
||||
|
||||
166
deployment/ansible/playbooks/rollback.yml
Normal file
166
deployment/ansible/playbooks/rollback.yml
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
- name: Rollback Application Deployment
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
rollback_to_version: "{{ rollback_to_version | default('previous') }}"
|
||||
app_stack_path: "{{ deploy_user_home }}/deployment/stacks/application"
|
||||
|
||||
pre_tasks:
|
||||
- name: Optionally load registry credentials from encrypted vault
|
||||
include_vars:
|
||||
file: "{{ playbook_dir }}/../secrets/production.vault.yml"
|
||||
no_log: yes
|
||||
ignore_errors: yes
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
|
||||
- name: Derive docker registry credentials from vault when not provided
|
||||
set_fact:
|
||||
docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(omit)) }}"
|
||||
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(omit)) }}"
|
||||
|
||||
- name: Check Docker service
|
||||
systemd:
|
||||
name: docker
|
||||
state: started
|
||||
register: docker_service
|
||||
become: yes
|
||||
|
||||
- name: Fail if Docker is not running
|
||||
fail:
|
||||
msg: "Docker service is not running - cannot perform rollback"
|
||||
when: docker_service.status.ActiveState != 'active'
|
||||
|
||||
- name: Get list of available backups
|
||||
find:
|
||||
paths: "{{ backups_path }}"
|
||||
file_type: directory
|
||||
register: available_backups
|
||||
|
||||
- name: Fail if no backups available
|
||||
fail:
|
||||
msg: "No backup versions available for rollback"
|
||||
when: available_backups.matched == 0
|
||||
|
||||
- name: Sort backups by date (newest first)
|
||||
set_fact:
|
||||
sorted_backups: "{{ available_backups.files | sort(attribute='mtime', reverse=true) }}"
|
||||
|
||||
tasks:
|
||||
- name: Determine rollback target
|
||||
set_fact:
|
||||
rollback_backup: "{{ sorted_backups[1] if rollback_to_version == 'previous' else sorted_backups | selectattr('path', 'search', rollback_to_version) | first }}"
|
||||
when: sorted_backups | length > 1
|
||||
|
||||
- name: Fail if rollback target not found
|
||||
fail:
|
||||
msg: "Cannot determine rollback target. Available backups: {{ sorted_backups | map(attribute='path') | list }}"
|
||||
when: rollback_backup is not defined
|
||||
|
||||
- name: Load rollback metadata
|
||||
slurp:
|
||||
src: "{{ rollback_backup.path }}/deployment_metadata.txt"
|
||||
register: rollback_metadata
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Parse rollback image from metadata
|
||||
set_fact:
|
||||
rollback_image: "{{ rollback_metadata.content | b64decode | regex_search('Deployed Image: ([^\\n]+)', '\\1') | first }}"
|
||||
when: rollback_metadata is succeeded
|
||||
|
||||
- name: Alternative: Parse rollback image from docker-compose config backup
|
||||
set_fact:
|
||||
rollback_image: "{{ rollback_metadata.content | b64decode | regex_search('image:\\s+([^:]+):([^\\n]+)', '\\1:\\2') | first }}"
|
||||
when:
|
||||
- rollback_metadata is succeeded
|
||||
- rollback_image is not defined
|
||||
|
||||
- name: Fail if cannot determine rollback image
|
||||
fail:
|
||||
msg: "Cannot determine image to rollback to from backup: {{ rollback_backup.path }}"
|
||||
when: rollback_image is not defined or rollback_image == ''
|
||||
|
||||
- name: Display rollback information
|
||||
debug:
|
||||
msg:
|
||||
- "Rolling back to previous version"
|
||||
- "Backup: {{ rollback_backup.path }}"
|
||||
- "Image: {{ rollback_image }}"
|
||||
|
||||
- name: Login to Docker registry (if credentials provided)
|
||||
community.docker.docker_login:
|
||||
registry_url: "{{ docker_registry_url }}"
|
||||
username: "{{ docker_registry_username }}"
|
||||
password: "{{ docker_registry_password }}"
|
||||
no_log: yes
|
||||
when:
|
||||
- docker_registry_username is defined
|
||||
- docker_registry_password is defined
|
||||
|
||||
- name: Pull rollback image
|
||||
community.docker.docker_image:
|
||||
name: "{{ rollback_image.split(':')[0] }}"
|
||||
tag: "{{ rollback_image.split(':')[1] }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
register: image_pull
|
||||
|
||||
- name: Update docker-compose.yml with rollback image
|
||||
replace:
|
||||
path: "{{ app_stack_path }}/docker-compose.yml"
|
||||
regexp: '^(\s+image:\s+){{ app_image }}:.*$'
|
||||
replace: '\1{{ rollback_image }}'
|
||||
register: compose_updated
|
||||
|
||||
- name: Restart application stack with rollback image
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ app_stack_path }}"
|
||||
state: present
|
||||
pull: always
|
||||
recreate: always
|
||||
remove_orphans: yes
|
||||
register: stack_rollback
|
||||
when: compose_updated.changed
|
||||
|
||||
- name: Wait for services to be healthy after rollback
|
||||
wait_for:
|
||||
timeout: 60
|
||||
changed_when: false
|
||||
|
||||
- name: Get current running image
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: current_image
|
||||
changed_when: false
|
||||
|
||||
- name: Record rollback event
|
||||
copy:
|
||||
content: |
|
||||
Rollback Timestamp: {{ ansible_date_time.iso8601 }}
|
||||
Rolled back from: {{ sorted_backups[0].path | basename }}
|
||||
Rolled back to: {{ rollback_backup.path | basename }}
|
||||
Rollback Image: {{ rollback_image }}
|
||||
Current Image: {{ current_image.stdout }}
|
||||
dest: "{{ backups_path }}/rollback_{{ ansible_date_time.epoch }}.txt"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0644'
|
||||
|
||||
post_tasks:
|
||||
- name: Display rollback summary
|
||||
debug:
|
||||
msg:
|
||||
- "✅ Rollback completed successfully!"
|
||||
- "Rolled back to: {{ rollback_image }}"
|
||||
- "From backup: {{ rollback_backup.path }}"
|
||||
- "Current running image: {{ current_image.stdout }}"
|
||||
- "Health check URL: {{ health_check_url }}"
|
||||
|
||||
- name: Recommend health check
|
||||
debug:
|
||||
msg: "⚠️ Please verify application health at {{ health_check_url }}"
|
||||
210
deployment/ansible/playbooks/setup-infrastructure.yml
Normal file
210
deployment/ansible/playbooks/setup-infrastructure.yml
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
- name: Deploy Infrastructure Stacks on Production Server
|
||||
hosts: production
|
||||
become: no
|
||||
gather_facts: yes
|
||||
|
||||
vars:
|
||||
stacks_base_path: "~/deployment/stacks"
|
||||
wait_timeout: 60
|
||||
|
||||
tasks:
|
||||
- name: Check if deployment stacks directory exists
|
||||
stat:
|
||||
path: "{{ stacks_base_path }}"
|
||||
register: stacks_dir
|
||||
|
||||
- name: Fail if stacks directory doesn't exist
|
||||
fail:
|
||||
msg: "Deployment stacks directory not found at {{ stacks_base_path }}"
|
||||
when: not stacks_dir.stat.exists
|
||||
|
||||
# Create external network required by all stacks
|
||||
- name: Create traefik-public network
|
||||
community.docker.docker_network:
|
||||
name: traefik-public
|
||||
driver: bridge
|
||||
state: present
|
||||
|
||||
# 1. Deploy Traefik (Reverse Proxy & SSL)
|
||||
- name: Deploy Traefik stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stacks_base_path }}/traefik"
|
||||
state: present
|
||||
pull: always
|
||||
register: traefik_output
|
||||
|
||||
- name: Wait for Traefik to be ready
|
||||
wait_for:
|
||||
timeout: "{{ wait_timeout }}"
|
||||
when: traefik_output.changed
|
||||
|
||||
- name: Check Traefik logs for readiness hint
|
||||
shell: docker compose logs traefik 2>&1 | grep -Ei "(configuration loaded|traefik[[:space:]]v|Starting provider|Server configuration loaded)" || true
|
||||
args:
|
||||
chdir: "{{ stacks_base_path }}/traefik"
|
||||
register: traefik_logs
|
||||
until: traefik_logs.stdout != ""
|
||||
retries: 6
|
||||
delay: 10
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
# 2. Deploy PostgreSQL (Database)
|
||||
- name: Deploy PostgreSQL stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stacks_base_path }}/postgresql"
|
||||
state: present
|
||||
pull: always
|
||||
register: postgres_output
|
||||
|
||||
- name: Wait for PostgreSQL to be ready
|
||||
wait_for:
|
||||
timeout: "{{ wait_timeout }}"
|
||||
when: postgres_output.changed
|
||||
|
||||
- name: Check PostgreSQL logs for readiness
|
||||
shell: docker compose logs postgres 2>&1 | grep -Ei "(ready to accept connections|database system is ready)" || true
|
||||
args:
|
||||
chdir: "{{ stacks_base_path }}/postgresql"
|
||||
register: postgres_logs
|
||||
until: postgres_logs.stdout != ""
|
||||
retries: 6
|
||||
delay: 10
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
# 3. Deploy Docker Registry (Private Registry)
|
||||
- name: Ensure Registry auth directory exists
|
||||
file:
|
||||
path: "{{ stacks_base_path }}/registry/auth"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
become: yes
|
||||
|
||||
- name: Create Registry htpasswd file if missing
|
||||
shell: |
|
||||
if [ ! -f {{ stacks_base_path }}/registry/auth/htpasswd ]; then
|
||||
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin registry-secure-password-2025 > {{ stacks_base_path }}/registry/auth/htpasswd
|
||||
chmod 644 {{ stacks_base_path }}/registry/auth/htpasswd
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become: yes
|
||||
changed_when: true
|
||||
register: registry_auth_created
|
||||
|
||||
- name: Deploy Docker Registry stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stacks_base_path }}/registry"
|
||||
state: present
|
||||
pull: always
|
||||
register: registry_output
|
||||
|
||||
- name: Wait for Docker Registry to be ready
|
||||
wait_for:
|
||||
timeout: "{{ wait_timeout }}"
|
||||
when: registry_output.changed
|
||||
|
||||
- name: Check Registry logs for readiness
|
||||
shell: docker compose logs registry 2>&1 | grep -Ei "(listening on|listening at|http server)" || true
|
||||
args:
|
||||
chdir: "{{ stacks_base_path }}/registry"
|
||||
register: registry_logs
|
||||
until: registry_logs.stdout != ""
|
||||
retries: 6
|
||||
delay: 10
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Verify Registry is accessible
|
||||
uri:
|
||||
url: "http://127.0.0.1:5000/v2/_catalog"
|
||||
user: admin
|
||||
password: registry-secure-password-2025
|
||||
status_code: 200
|
||||
timeout: 5
|
||||
register: registry_check
|
||||
ignore_errors: yes
|
||||
changed_when: false
|
||||
|
||||
- name: Display Registry status
|
||||
debug:
|
||||
msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}"
|
||||
|
||||
# 4. Deploy Gitea (CRITICAL - Git Server + MySQL + Redis)
|
||||
- name: Deploy Gitea stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stacks_base_path }}/gitea"
|
||||
state: present
|
||||
pull: always
|
||||
register: gitea_output
|
||||
|
||||
- name: Wait for Gitea to be ready
|
||||
wait_for:
|
||||
timeout: "{{ wait_timeout }}"
|
||||
when: gitea_output.changed
|
||||
|
||||
- name: Check Gitea logs for readiness
|
||||
shell: docker compose logs gitea 2>&1 | grep -Ei "(Listen:|Server is running|Starting server)" || true
|
||||
args:
|
||||
chdir: "{{ stacks_base_path }}/gitea"
|
||||
register: gitea_logs
|
||||
until: gitea_logs.stdout != ""
|
||||
retries: 12
|
||||
delay: 10
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
# 5. Deploy Monitoring (Portainer + Grafana + Prometheus)
|
||||
- name: Deploy Monitoring stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stacks_base_path }}/monitoring"
|
||||
state: present
|
||||
pull: always
|
||||
register: monitoring_output
|
||||
|
||||
- name: Wait for Monitoring to be ready
|
||||
wait_for:
|
||||
timeout: "{{ wait_timeout }}"
|
||||
when: monitoring_output.changed
|
||||
|
||||
# Verification
|
||||
- name: List all running containers
|
||||
command: >
|
||||
docker ps --format 'table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}'
|
||||
register: docker_ps_output
|
||||
|
||||
- name: Display running containers
|
||||
debug:
|
||||
msg: "{{ docker_ps_output.stdout_lines }}"
|
||||
|
||||
- name: Verify Gitea accessibility via HTTPS
|
||||
uri:
|
||||
url: https://git.michaelschiemer.de
|
||||
method: GET
|
||||
validate_certs: no
|
||||
status_code: 200
|
||||
timeout: 10
|
||||
register: gitea_http_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display Gitea accessibility status
|
||||
debug:
|
||||
msg: "Gitea HTTPS check: {{ 'SUCCESS' if gitea_http_check.status == 200 else 'FAILED - Status: ' + (gitea_http_check.status|string) }}"
|
||||
|
||||
- name: Summary
|
||||
debug:
|
||||
msg:
|
||||
- "=== Infrastructure Deployment Complete ==="
|
||||
- "Traefik: {{ 'Deployed' if traefik_output.changed else 'Already running' }}"
|
||||
- "PostgreSQL: {{ 'Deployed' if postgres_output.changed else 'Already running' }}"
|
||||
- "Docker Registry: {{ 'Deployed' if registry_output.changed else 'Already running' }}"
|
||||
- "Gitea: {{ 'Deployed' if gitea_output.changed else 'Already running' }}"
|
||||
- "Monitoring: {{ 'Deployed' if monitoring_output.changed else 'Already running' }}"
|
||||
- ""
|
||||
- "Next Steps:"
|
||||
- "1. Access Gitea at: https://git.michaelschiemer.de"
|
||||
- "2. Complete Gitea setup wizard if first-time deployment"
|
||||
- "3. Navigate to Admin > Actions > Runners to get registration token"
|
||||
- "4. Continue with Phase 1 - Gitea Runner Setup"
|
||||
92
deployment/ansible/playbooks/setup-production-secrets.yml
Normal file
92
deployment/ansible/playbooks/setup-production-secrets.yml
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
- name: Setup Production Secrets
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: yes
|
||||
|
||||
vars:
|
||||
vault_file: "{{ playbook_dir }}/../secrets/production.vault.yml"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify vault file exists
|
||||
stat:
|
||||
path: "{{ vault_file }}"
|
||||
register: vault_stat
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
|
||||
- name: Fail if vault file missing
|
||||
fail:
|
||||
msg: "Vault file not found at {{ vault_file }}"
|
||||
when: not vault_stat.stat.exists
|
||||
|
||||
tasks:
|
||||
- name: Detect Docker Swarm mode
|
||||
shell: docker info -f '{{ "{{" }}.Swarm.LocalNodeState{{ "}}" }}'
|
||||
register: swarm_state
|
||||
changed_when: false
|
||||
|
||||
- name: Set fact if swarm is active
|
||||
set_fact:
|
||||
swarm_active: "{{ swarm_state.stdout | lower == 'active' }}"
|
||||
|
||||
- name: Load encrypted secrets
|
||||
include_vars:
|
||||
file: "{{ vault_file }}"
|
||||
no_log: yes
|
||||
|
||||
- name: Ensure secrets directory exists
|
||||
file:
|
||||
path: "{{ secrets_path }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0700'
|
||||
|
||||
- name: Create .env.production file
|
||||
template:
|
||||
src: "{{ playbook_dir }}/../templates/.env.production.j2"
|
||||
dest: "{{ secrets_path }}/.env.production"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0600'
|
||||
no_log: yes
|
||||
|
||||
- name: Create Docker secrets from vault (disabled for compose-only deployment)
|
||||
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: mail_password
|
||||
value: "{{ vault_mail_password }}"
|
||||
no_log: yes
|
||||
when: false
|
||||
|
||||
- name: Set secure permissions on secrets directory
|
||||
file:
|
||||
path: "{{ secrets_path }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0700'
|
||||
recurse: yes
|
||||
|
||||
- name: Verify Docker secrets (skipped)
|
||||
command: docker secret ls --format '{{ "{{" }}.Name{{ "}}" }}'
|
||||
register: docker_secrets
|
||||
changed_when: false
|
||||
when: false
|
||||
|
||||
- name: Display deployed Docker secrets (skipped)
|
||||
debug:
|
||||
msg: "Deployed secrets: {{ docker_secrets.stdout_lines | default([]) }}"
|
||||
when: false
|
||||
86
deployment/ansible/playbooks/setup-ssl-certificates.yml
Normal file
86
deployment/ansible/playbooks/setup-ssl-certificates.yml
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
- name: Setup Let's Encrypt SSL Certificates via Traefik
|
||||
hosts: production
|
||||
become: no
|
||||
gather_facts: yes
|
||||
|
||||
vars:
|
||||
domains:
|
||||
- git.michaelschiemer.de
|
||||
- michaelschiemer.de
|
||||
acme_email: kontakt@michaelschiemer.de
|
||||
|
||||
tasks:
|
||||
- name: Check if acme.json exists and is a file
|
||||
stat:
|
||||
path: "{{ deploy_user_home }}/de iployment/stacks/traefik/acme.json"
|
||||
register: acme_stat
|
||||
|
||||
- name: Remove acme.json if it's a directory
|
||||
file:
|
||||
path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json"
|
||||
state: absent
|
||||
become: yes
|
||||
when: acme_stat.stat.exists and acme_stat.stat.isdir
|
||||
|
||||
- name: Ensure Traefik acme.json exists and has correct permissions
|
||||
file:
|
||||
path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json"
|
||||
state: touch
|
||||
mode: '0600'
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
become: yes
|
||||
|
||||
- name: Verify Traefik is running
|
||||
command: docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml ps traefik
|
||||
register: traefik_status
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if Traefik is not running
|
||||
fail:
|
||||
msg: "Traefik is not running. Please start it first."
|
||||
when: traefik_status.rc != 0 or "Up" not in traefik_status.stdout
|
||||
|
||||
- name: Force Traefik to reload configuration
|
||||
command: docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml restart traefik
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for Traefik to be ready
|
||||
wait_for:
|
||||
timeout: 10
|
||||
changed_when: false
|
||||
|
||||
- name: Trigger certificate request by accessing each domain
|
||||
uri:
|
||||
url: "https://{{ item }}"
|
||||
method: GET
|
||||
validate_certs: no
|
||||
timeout: 5
|
||||
status_code: [200, 301, 302, 303, 404, 502, 503]
|
||||
loop: "{{ domains }}"
|
||||
register: certificate_trigger
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Wait for ACME certificate generation (30 seconds)
|
||||
wait_for:
|
||||
timeout: 30
|
||||
changed_when: false
|
||||
|
||||
- name: Check if acme.json contains certificates
|
||||
stat:
|
||||
path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json"
|
||||
register: acme_file
|
||||
|
||||
- name: Display certificate status
|
||||
debug:
|
||||
msg: |
|
||||
Certificate setup triggered.
|
||||
Traefik will request Let's Encrypt certificates for:
|
||||
{{ domains | join(', ') }}
|
||||
|
||||
Check Traefik logs to see certificate generation progress:
|
||||
docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml logs traefik | grep -i acme
|
||||
|
||||
Certificates should be ready within 1-2 minutes.
|
||||
53
deployment/ansible/playbooks/sync-stacks.yml
Normal file
53
deployment/ansible/playbooks/sync-stacks.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
- name: Sync Infrastructure Stacks to Production Server
|
||||
hosts: production
|
||||
become: no
|
||||
gather_facts: yes
|
||||
|
||||
vars:
|
||||
local_stacks_path: "/home/michael/dev/michaelschiemer/deployment/stacks"
|
||||
remote_stacks_path: "~/deployment"
|
||||
|
||||
tasks:
|
||||
- name: Ensure deployment directory exists on production
|
||||
file:
|
||||
path: "{{ remote_stacks_path }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Sync stacks directory to production server
|
||||
synchronize:
|
||||
src: "{{ local_stacks_path }}"
|
||||
dest: "{{ remote_stacks_path }}/"
|
||||
delete: no
|
||||
recursive: yes
|
||||
rsync_opts:
|
||||
- "--chmod=D755,F644"
|
||||
- "--exclude=.git"
|
||||
- "--exclude=*.log"
|
||||
- "--exclude=data/"
|
||||
- "--exclude=volumes/"
|
||||
|
||||
- name: Ensure executable permissions on PostgreSQL backup scripts
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- "{{ remote_stacks_path }}/stacks/postgresql/scripts/backup-entrypoint.sh"
|
||||
- "{{ remote_stacks_path }}/stacks/postgresql/scripts/backup.sh"
|
||||
- "{{ remote_stacks_path }}/stacks/postgresql/scripts/restore.sh"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Verify stacks directory exists on production
|
||||
stat:
|
||||
path: "{{ remote_stacks_path }}/stacks"
|
||||
register: stacks_dir
|
||||
|
||||
- name: Display sync results
|
||||
debug:
|
||||
msg:
|
||||
- "=== Stacks Synchronization Complete ==="
|
||||
- "Stacks directory exists: {{ stacks_dir.stat.exists }}"
|
||||
- "Path: {{ remote_stacks_path }}/stacks"
|
||||
- ""
|
||||
- "Next: Run infrastructure deployment playbook"
|
||||
Reference in New Issue
Block a user