fix: staging deployment configuration and redis secrets handling
This commit is contained in:
250
deployment/ansible/playbooks/debug-staging-redis-secrets.yml
Normal file
250
deployment/ansible/playbooks/debug-staging-redis-secrets.yml
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
- name: Debug Staging Redis Secrets Configuration
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
tasks:
|
||||
- name: Check staging stack directory
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== Staging Stack Directory ==="
|
||||
pwd
|
||||
ls -la
|
||||
register: dir_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display directory contents
|
||||
debug:
|
||||
msg: "{{ dir_check.stdout_lines }}"
|
||||
|
||||
- name: Check if docker-compose files exist
|
||||
stat:
|
||||
path: "{{ item }}"
|
||||
vars:
|
||||
deployment_path: "~/deployment/stacks/staging"
|
||||
with_items:
|
||||
- "{{ deployment_path }}/docker-compose.base.yml"
|
||||
- "{{ deployment_path }}/docker-compose.staging.yml"
|
||||
register: compose_files
|
||||
|
||||
- name: Display compose file status
|
||||
debug:
|
||||
msg: "{{ item.stat.exists | default(false) }}: {{ item.item }}"
|
||||
with_items: "{{ compose_files.results }}"
|
||||
|
||||
- name: Check docker-compose.staging.yml configuration
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== Checking docker-compose.staging.yml for Redis secrets ==="
|
||||
if [ -f docker-compose.staging.yml ]; then
|
||||
echo "--- REDIS_PASSWORD_FILE in environment ---"
|
||||
grep -A 5 "staging-app:" docker-compose.staging.yml | grep -A 10 "environment:" | grep "REDIS_PASSWORD_FILE" || echo "REDIS_PASSWORD_FILE not found in staging-app environment"
|
||||
echo ""
|
||||
echo "--- Secrets section for staging-app ---"
|
||||
grep -A 10 "staging-app:" docker-compose.staging.yml | grep -A 15 "secrets:" | head -10 || echo "Secrets section not found"
|
||||
echo ""
|
||||
echo "--- Secrets definitions at bottom ---"
|
||||
tail -30 docker-compose.staging.yml | grep -A 5 "redis_password:" || echo "redis_password secret definition not found"
|
||||
else
|
||||
echo "docker-compose.staging.yml NOT FOUND"
|
||||
fi
|
||||
register: compose_config
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display compose configuration
|
||||
debug:
|
||||
msg: "{{ compose_config.stdout_lines }}"
|
||||
|
||||
- name: Check if secrets directory and files exist
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== Secrets Directory ==="
|
||||
if [ -d secrets ]; then
|
||||
echo "secrets/ directory exists"
|
||||
ls -la secrets/
|
||||
echo ""
|
||||
echo "--- redis_password.txt content check ---"
|
||||
if [ -f secrets/redis_password.txt ]; then
|
||||
echo "secrets/redis_password.txt exists"
|
||||
FILE_SIZE=$(stat -f%z secrets/redis_password.txt 2>/dev/null || stat -c%s secrets/redis_password.txt 2>/dev/null || echo "unknown")
|
||||
CONTENT_LENGTH=$(wc -c < secrets/redis_password.txt | tr -d ' ')
|
||||
echo "File size: $FILE_SIZE bytes"
|
||||
echo "Content length: $CONTENT_LENGTH bytes"
|
||||
# Show first 5 chars (for debugging)
|
||||
FIRST_CHARS=$(head -c 5 secrets/redis_password.txt)
|
||||
echo "First 5 chars: $FIRST_CHARS"
|
||||
else
|
||||
echo "secrets/redis_password.txt NOT FOUND"
|
||||
fi
|
||||
else
|
||||
echo "secrets/ directory DOES NOT EXIST"
|
||||
fi
|
||||
register: secrets_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display secrets check
|
||||
debug:
|
||||
msg: "{{ secrets_check.stdout_lines }}"
|
||||
|
||||
- name: Check if staging-app container is running
|
||||
shell: |
|
||||
docker ps --filter "name=staging-app" --format "{{.Names}}\t{{.Status}}\t{{.Image}}"
|
||||
register: container_status
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display container status
|
||||
debug:
|
||||
msg: "{{ container_status.stdout_lines }}"
|
||||
|
||||
- name: Check Docker secrets mounted in staging-app container
|
||||
shell: |
|
||||
echo "=== Docker Secrets in staging-app Container ==="
|
||||
if docker ps --filter "name=staging-app" --format "{{.Names}}" | grep -q staging-app; then
|
||||
echo "--- Checking /run/secrets/ directory ---"
|
||||
docker exec staging-app ls -la /run/secrets/ 2>&1 || echo "Cannot access /run/secrets/"
|
||||
echo ""
|
||||
echo "--- Checking redis_password secret file ---"
|
||||
docker exec staging-app cat /run/secrets/redis_password 2>&1 | head -c 20 || echo "redis_password secret NOT FOUND or NOT READABLE"
|
||||
echo "..."
|
||||
echo ""
|
||||
echo "--- File exists check ---"
|
||||
docker exec staging-app test -f /run/secrets/redis_password && echo "redis_password file EXISTS" || echo "redis_password file DOES NOT EXIST"
|
||||
docker exec staging-app test -r /run/secrets/redis_password && echo "redis_password file is READABLE" || echo "redis_password file is NOT READABLE"
|
||||
else
|
||||
echo "staging-app container is NOT RUNNING"
|
||||
fi
|
||||
register: secrets_mounted
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display secrets mount status
|
||||
debug:
|
||||
msg: "{{ secrets_mounted.stdout_lines }}"
|
||||
|
||||
- name: Check Environment Variables in staging-app container
|
||||
shell: |
|
||||
echo "=== Environment Variables in staging-app ==="
|
||||
if docker ps --filter "name=staging-app" --format "{{.Names}}" | grep -q staging-app; then
|
||||
echo "--- Redis-related environment variables ---"
|
||||
docker exec staging-app env | grep -E "(REDIS_|CACHE_|SESSION_|QUEUE_)" || echo "No Redis env vars found"
|
||||
echo ""
|
||||
echo "--- *_FILE environment variables ---"
|
||||
docker exec staging-app env | grep "_FILE" || echo "No _FILE env vars found"
|
||||
echo ""
|
||||
echo "--- All environment variables (first 50) ---"
|
||||
docker exec staging-app env | sort | head -50 || echo "Cannot read environment"
|
||||
else
|
||||
echo "Container not running"
|
||||
fi
|
||||
register: env_vars
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display environment variables
|
||||
debug:
|
||||
msg: "{{ env_vars.stdout_lines }}"
|
||||
|
||||
- name: Test PHP environment resolution (check DockerSecretsResolver)
|
||||
shell: |
|
||||
echo "=== Testing PHP Environment Resolution ==="
|
||||
docker exec staging-app php -r "
|
||||
// Simulate what the Framework does
|
||||
echo '=== System Environment Check ===' . PHP_EOL;
|
||||
echo 'getenv(\"REDIS_PASSWORD_FILE\"): ' . (getenv('REDIS_PASSWORD_FILE') ?: 'NOT SET') . PHP_EOL;
|
||||
echo 'getenv(\"REDIS_PASSWORD\"): ' . (getenv('REDIS_PASSWORD') ? 'SET (length: ' . strlen(getenv('REDIS_PASSWORD')) . ')' : 'NOT SET') . PHP_EOL;
|
||||
echo PHP_EOL;
|
||||
|
||||
echo '=== $_ENV Check ===' . PHP_EOL;
|
||||
echo 'isset($_ENV[\"REDIS_PASSWORD_FILE\"]): ' . (isset(\$_ENV['REDIS_PASSWORD_FILE']) ? 'YES: ' . \$_ENV['REDIS_PASSWORD_FILE'] : 'NO') . PHP_EOL;
|
||||
echo 'isset($_ENV[\"REDIS_PASSWORD\"]): ' . (isset(\$_ENV['REDIS_PASSWORD']) ? 'YES (length: ' . strlen(\$_ENV['REDIS_PASSWORD']) . ')' : 'NO') . PHP_EOL;
|
||||
echo PHP_EOL;
|
||||
|
||||
echo '=== $_SERVER Check ===' . PHP_EOL;
|
||||
echo 'isset($_SERVER[\"REDIS_PASSWORD_FILE\"]): ' . (isset(\$_SERVER['REDIS_PASSWORD_FILE']) ? 'YES: ' . \$_SERVER['REDIS_PASSWORD_FILE'] : 'NO') . PHP_EOL;
|
||||
echo 'isset($_SERVER[\"REDIS_PASSWORD\"]): ' . (isset(\$_SERVER['REDIS_PASSWORD']) ? 'YES (length: ' . strlen(\$_SERVER['REDIS_PASSWORD']) . ')' : 'NO') . PHP_EOL;
|
||||
echo PHP_EOL;
|
||||
|
||||
echo '=== Docker Secrets File Check ===' . PHP_EOL;
|
||||
\$secret_file = '/run/secrets/redis_password';
|
||||
echo 'File path: ' . \$secret_file . PHP_EOL;
|
||||
echo 'File exists: ' . (file_exists(\$secret_file) ? 'YES' : 'NO') . PHP_EOL;
|
||||
if (file_exists(\$secret_file)) {
|
||||
echo 'File readable: ' . (is_readable(\$secret_file) ? 'YES' : 'NO') . PHP_EOL;
|
||||
\$content = file_get_contents(\$secret_file);
|
||||
if (\$content !== false) {
|
||||
echo 'File content length: ' . strlen(trim(\$content)) . PHP_EOL;
|
||||
echo 'File content (first 10 chars): ' . substr(trim(\$content), 0, 10) . '...' . PHP_EOL;
|
||||
} else {
|
||||
echo 'File content: COULD NOT READ' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
echo PHP_EOL;
|
||||
|
||||
// Test DockerSecretsResolver logic
|
||||
echo '=== DockerSecretsResolver Simulation ===' . PHP_EOL;
|
||||
\$variables = getenv();
|
||||
\$file_key = 'REDIS_PASSWORD_FILE';
|
||||
if (isset(\$variables[\$file_key])) {
|
||||
\$file_path = \$variables[\$file_key];
|
||||
echo 'REDIS_PASSWORD_FILE found: ' . \$file_path . PHP_EOL;
|
||||
if (file_exists(\$file_path) && is_readable(\$file_path)) {
|
||||
\$secret_value = trim(file_get_contents(\$file_path));
|
||||
echo 'Secret resolved: YES (length: ' . strlen(\$secret_value) . ')' . PHP_EOL;
|
||||
echo 'Secret value (first 10 chars): ' . substr(\$secret_value, 0, 10) . '...' . PHP_EOL;
|
||||
} else {
|
||||
echo 'Secret resolved: NO (file not accessible)' . PHP_EOL;
|
||||
}
|
||||
} else {
|
||||
echo 'REDIS_PASSWORD_FILE NOT FOUND in environment' . PHP_EOL;
|
||||
}
|
||||
" 2>&1
|
||||
register: php_test
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display PHP environment test
|
||||
debug:
|
||||
msg: "{{ php_test.stdout_lines }}"
|
||||
|
||||
- name: Check staging-redis container configuration
|
||||
shell: |
|
||||
echo "=== Staging Redis Container ==="
|
||||
docker ps --filter "name=staging-redis" --format "{{.Names}}\t{{.Status}}"
|
||||
echo ""
|
||||
echo "=== Redis password requirement ==="
|
||||
docker exec staging-redis redis-cli CONFIG GET requirepass 2>&1 || echo "Cannot check Redis config"
|
||||
echo ""
|
||||
echo "=== Test Redis connection without password ==="
|
||||
docker exec staging-redis redis-cli PING 2>&1 || echo "Connection failed (password required)"
|
||||
register: redis_config
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display Redis configuration
|
||||
debug:
|
||||
msg: "{{ redis_config.stdout_lines }}"
|
||||
|
||||
- name: Check recent staging-app logs for Redis errors
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== Recent staging-app logs (Redis-related) ==="
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml logs staging-app --tail=100 2>&1 | grep -i -E "(redis|password|secret|auth|noauth)" | tail -30 || echo "No Redis-related logs found"
|
||||
register: app_logs
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display application logs
|
||||
debug:
|
||||
msg: "{{ app_logs.stdout_lines }}"
|
||||
|
||||
- name: Summary and Recommendations
|
||||
debug:
|
||||
msg:
|
||||
- "========================================"
|
||||
- "DEBUG SUMMARY"
|
||||
- "========================================"
|
||||
- "Check the output above for:"
|
||||
- "1. docker-compose.staging.yml has REDIS_PASSWORD_FILE=/run/secrets/redis_password"
|
||||
- "2. secrets/redis_password.txt exists and is readable"
|
||||
- "3. Container has /run/secrets/redis_password file mounted"
|
||||
- "4. Container environment has REDIS_PASSWORD_FILE variable set"
|
||||
- "5. PHP can read the secret file and resolve REDIS_PASSWORD"
|
||||
- "6. Redis container requires password (requirepass set)"
|
||||
- ""
|
||||
- "If any check fails, the issue is identified above."
|
||||
239
deployment/ansible/playbooks/deploy/production.yml
Normal file
239
deployment/ansible/playbooks/deploy/production.yml
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
- name: Deploy Application Update to Production via Docker Compose
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# These should be passed via -e from CI/CD
|
||||
application_environment: production
|
||||
application_compose_suffix: production.yml
|
||||
# app_stack_path is now defined in group_vars/production.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Set deployment variables
|
||||
set_fact:
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
git_commit_sha: "{{ git_commit_sha | default('unknown') }}"
|
||||
deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}"
|
||||
- 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(docker_registry_username_default)) }}"
|
||||
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(docker_registry_password_default)) }}"
|
||||
|
||||
- name: Ensure system packages are up to date
|
||||
include_role:
|
||||
name: system
|
||||
when: system_update_packages | bool
|
||||
|
||||
- name: Verify Docker is running
|
||||
systemd:
|
||||
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: Check if docker-compose.base.yml exists in application stack
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
register: compose_base_exists
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Check if docker-compose.production.yml exists in application stack
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.production.yml"
|
||||
register: compose_override_exists
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Fail if docker-compose files don't exist
|
||||
fail:
|
||||
msg: |
|
||||
Application Stack docker-compose files not found at {{ app_stack_path }}
|
||||
|
||||
Required files:
|
||||
- docker-compose.base.yml
|
||||
- docker-compose.production.yml
|
||||
|
||||
The Application Stack must be deployed first via:
|
||||
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
|
||||
|
||||
This will create the application stack with docker-compose files and .env file.
|
||||
when:
|
||||
- not (application_sync_files | default(false) | bool)
|
||||
- (not compose_base_exists.stat.exists or not compose_override_exists.stat.exists)
|
||||
|
||||
- name: Create backup directory
|
||||
file:
|
||||
path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
|
||||
tasks:
|
||||
- name: Verify docker-compose files exist
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
register: compose_base_check
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Verify docker-compose.production.yml exists
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.production.yml"
|
||||
register: compose_override_check
|
||||
when: not (application_sync_files | default(false) | bool)
|
||||
|
||||
- name: Fail if docker-compose files don't exist
|
||||
fail:
|
||||
msg: |
|
||||
Application Stack docker-compose files not found at {{ app_stack_path }}
|
||||
|
||||
Required files:
|
||||
- docker-compose.base.yml
|
||||
- docker-compose.production.yml
|
||||
|
||||
The Application Stack must be deployed first via:
|
||||
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
|
||||
|
||||
This will create the application stack with docker-compose files and .env file.
|
||||
when:
|
||||
- not (application_sync_files | default(false) | bool)
|
||||
- (not compose_base_check.stat.exists or not compose_override_check.stat.exists)
|
||||
|
||||
- name: Backup current deployment metadata
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.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.base.yml -f {{ app_stack_path }}/docker-compose.production.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
|
||||
when:
|
||||
- not (application_sync_files | default(false) | bool)
|
||||
- compose_base_exists.stat.exists | default(false)
|
||||
- compose_override_exists.stat.exists | default(false)
|
||||
|
||||
- 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
|
||||
ignore_errors: yes
|
||||
when:
|
||||
- docker_registry_username is defined
|
||||
- docker_registry_password is defined
|
||||
- docker_registry_username | length > 0
|
||||
- docker_registry_password | length > 0
|
||||
register: registry_login
|
||||
|
||||
- name: Pull new Docker image
|
||||
community.docker.docker_image:
|
||||
name: "{{ app_image }}"
|
||||
tag: "{{ image_tag }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
register: image_pull
|
||||
|
||||
- name: Verify image was pulled successfully
|
||||
fail:
|
||||
msg: "Failed to pull image {{ app_image }}:{{ image_tag }}"
|
||||
when: image_pull.failed
|
||||
|
||||
# Sync files first if application_sync_files=true (before updating docker-compose.production.yml)
|
||||
- name: Sync application stack files
|
||||
import_role:
|
||||
name: application
|
||||
vars:
|
||||
application_sync_files: "{{ application_sync_files | default(false) }}"
|
||||
application_compose_recreate: "never" # Don't recreate yet, just sync files
|
||||
application_remove_orphans: false
|
||||
when: application_sync_files | default(false) | bool
|
||||
|
||||
- name: Update docker-compose.production.yml with new image tag (all services)
|
||||
replace:
|
||||
path: "{{ app_stack_path }}/docker-compose.production.yml"
|
||||
# Match both localhost:5000 and registry.michaelschiemer.de (or any registry URL)
|
||||
regexp: '^(\s+image:\s+)(localhost:5000|registry\.michaelschiemer\.de|{{ docker_registry }})/{{ app_name }}:.*$'
|
||||
replace: '\1{{ app_image }}:{{ image_tag }}'
|
||||
# Always update to ensure localhost:5000 is used (registry only accessible via localhost)
|
||||
when: true
|
||||
register: compose_updated
|
||||
|
||||
- name: Redeploy application stack with new image
|
||||
import_role:
|
||||
name: application
|
||||
vars:
|
||||
application_sync_files: false # Already synced above, don't sync again
|
||||
application_compose_recreate: "always"
|
||||
application_remove_orphans: true
|
||||
|
||||
- name: Get deployed image information
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.production.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: deployed_image
|
||||
changed_when: false
|
||||
|
||||
- name: Record deployment metadata
|
||||
copy:
|
||||
content: |
|
||||
Deployment Timestamp: {{ deployment_timestamp }}
|
||||
Git Commit: {{ git_commit_sha }}
|
||||
Image Tag: {{ image_tag }}
|
||||
Deployed Image: {{ deployed_image.stdout }}
|
||||
Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }}
|
||||
Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}
|
||||
Health Status: {{ application_health_output if application_health_output != '' else 'All services healthy' }}
|
||||
Health Check HTTP Status: {{ application_healthcheck_status }}
|
||||
dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Cleanup old backups (keep last {{ max_rollback_versions | default(5) }})
|
||||
shell: |
|
||||
cd {{ backups_path }}
|
||||
ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions | default(5) + 1 }} | xargs -r rm -rf
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
post_tasks:
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "=== Production Deployment Summary ==="
|
||||
- "Image: {{ app_image }}:{{ image_tag }}"
|
||||
- "Commit: {{ git_commit_sha }}"
|
||||
- "Timestamp: {{ deployment_timestamp }}"
|
||||
- "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}"
|
||||
- "Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}"
|
||||
- "Health Output: {{ application_health_output if application_health_output != '' else 'All services healthy' }}"
|
||||
- "Health Check HTTP Status: {{ application_healthcheck_status }}"
|
||||
- "Health Check URL: {{ health_check_url | default('https://michaelschiemer.de/health') }}"
|
||||
- ""
|
||||
- "Next: Verify application is healthy"
|
||||
226
deployment/ansible/playbooks/deploy/staging.yml
Normal file
226
deployment/ansible/playbooks/deploy/staging.yml
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
- name: Deploy Application Update to Staging via Docker Compose
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# These should be passed via -e from CI/CD
|
||||
application_environment: staging
|
||||
application_compose_suffix: staging.yml
|
||||
# app_stack_path is now defined in group_vars/production.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Set deployment variables
|
||||
set_fact:
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
git_commit_sha: "{{ git_commit_sha | default('unknown') }}"
|
||||
deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}"
|
||||
- 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(docker_registry_username_default)) }}"
|
||||
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(docker_registry_password_default)) }}"
|
||||
|
||||
- name: Ensure system packages are up to date
|
||||
include_role:
|
||||
name: system
|
||||
when: system_update_packages | bool
|
||||
|
||||
- name: Verify Docker is running
|
||||
systemd:
|
||||
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: Set staging stack path
|
||||
set_fact:
|
||||
app_stack_path: "{{ staging_stack_path | default(stacks_base_path + '/staging') }}"
|
||||
backups_path: "{{ backups_base_path | default('~/deployment/backups') }}"
|
||||
|
||||
- name: Ensure application stack directory exists
|
||||
file:
|
||||
path: "{{ app_stack_path }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Check if docker-compose.base.yml exists in staging stack
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
register: compose_base_exists
|
||||
|
||||
- name: Check if docker-compose.staging.yml exists in staging stack
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.staging.yml"
|
||||
register: compose_override_exists
|
||||
|
||||
- name: Fail if docker-compose files don't exist
|
||||
fail:
|
||||
msg: |
|
||||
Staging Stack docker-compose files not found at {{ app_stack_path }}
|
||||
|
||||
Required files:
|
||||
- docker-compose.base.yml
|
||||
- docker-compose.staging.yml
|
||||
|
||||
The Staging Stack must be deployed first via:
|
||||
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
|
||||
|
||||
This will create the staging stack with docker-compose files and .env file.
|
||||
when:
|
||||
- not compose_base_exists.stat.exists or not compose_override_exists.stat.exists
|
||||
|
||||
- name: Create backup directory
|
||||
file:
|
||||
path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
|
||||
tasks:
|
||||
- name: Verify docker-compose files exist
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
register: compose_base_check
|
||||
|
||||
- name: Verify docker-compose.staging.yml exists
|
||||
stat:
|
||||
path: "{{ app_stack_path }}/docker-compose.staging.yml"
|
||||
register: compose_override_check
|
||||
|
||||
- name: Fail if docker-compose files don't exist
|
||||
fail:
|
||||
msg: |
|
||||
Staging Stack docker-compose files not found at {{ app_stack_path }}
|
||||
|
||||
Required files:
|
||||
- docker-compose.base.yml
|
||||
- docker-compose.staging.yml
|
||||
|
||||
The Staging Stack must be deployed first via:
|
||||
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml
|
||||
|
||||
This will create the staging stack with docker-compose files and .env file.
|
||||
when:
|
||||
- not compose_base_check.stat.exists or not compose_override_check.stat.exists
|
||||
|
||||
- name: Backup current deployment metadata
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.staging.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.base.yml -f {{ app_stack_path }}/docker-compose.staging.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)
|
||||
community.docker.docker_login:
|
||||
registry_url: "{{ docker_registry_url }}"
|
||||
username: "{{ docker_registry_username }}"
|
||||
password: "{{ docker_registry_password }}"
|
||||
no_log: yes
|
||||
ignore_errors: yes
|
||||
when:
|
||||
- docker_registry_username is defined
|
||||
- docker_registry_password is defined
|
||||
- docker_registry_username | length > 0
|
||||
- docker_registry_password | length > 0
|
||||
register: registry_login
|
||||
|
||||
- name: Pull new Docker image
|
||||
community.docker.docker_image:
|
||||
name: "{{ app_image }}"
|
||||
tag: "{{ image_tag }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
register: image_pull
|
||||
|
||||
- name: Verify image was pulled successfully
|
||||
fail:
|
||||
msg: "Failed to pull image {{ app_image }}:{{ image_tag }}"
|
||||
when: image_pull.failed
|
||||
|
||||
- name: Update docker-compose.staging.yml with new image tag (all services)
|
||||
replace:
|
||||
path: "{{ app_stack_path }}/docker-compose.staging.yml"
|
||||
# Match both localhost:5000 and registry.michaelschiemer.de (or any registry URL)
|
||||
regexp: '^(\s+image:\s+)(localhost:5000|registry\.michaelschiemer\.de|{{ docker_registry }})/{{ app_name }}:.*$'
|
||||
replace: '\1{{ app_image }}:{{ image_tag }}'
|
||||
register: compose_updated
|
||||
|
||||
- name: Redeploy staging stack with new image
|
||||
import_role:
|
||||
name: application
|
||||
vars:
|
||||
application_sync_files: false
|
||||
application_compose_recreate: "always"
|
||||
application_remove_orphans: true
|
||||
application_stack_path: "{{ app_stack_path }}"
|
||||
application_compose_files:
|
||||
- "{{ app_stack_path }}/docker-compose.base.yml"
|
||||
- "{{ app_stack_path }}/docker-compose.staging.yml"
|
||||
|
||||
- name: Get deployed image information
|
||||
shell: |
|
||||
docker compose -f {{ app_stack_path }}/docker-compose.base.yml -f {{ app_stack_path }}/docker-compose.staging.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: deployed_image
|
||||
changed_when: false
|
||||
|
||||
- name: Record deployment metadata
|
||||
copy:
|
||||
content: |
|
||||
Deployment Timestamp: {{ deployment_timestamp }}
|
||||
Git Commit: {{ git_commit_sha }}
|
||||
Image Tag: {{ image_tag }}
|
||||
Deployed Image: {{ deployed_image.stdout }}
|
||||
Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }}
|
||||
Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}
|
||||
Health Status: {{ application_health_output if application_health_output != '' else 'All services healthy' }}
|
||||
Health Check HTTP Status: {{ application_healthcheck_status }}
|
||||
dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Cleanup old backups (keep last {{ max_rollback_versions | default(5) }})
|
||||
shell: |
|
||||
cd {{ backups_path }}
|
||||
ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions | default(5) + 1 }} | xargs -r rm -rf
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
post_tasks:
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "=== Staging Deployment Summary ==="
|
||||
- "Image: {{ app_image }}:{{ image_tag }}"
|
||||
- "Commit: {{ git_commit_sha }}"
|
||||
- "Timestamp: {{ deployment_timestamp }}"
|
||||
- "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}"
|
||||
- "Stack Deploy: {{ 'UPDATED' if application_stack_changed else 'NO_CHANGE' }}"
|
||||
- "Health Output: {{ application_health_output if application_health_output != '' else 'All services healthy' }}"
|
||||
- "Health Check HTTP Status: {{ application_healthcheck_status }}"
|
||||
- "Health Check URL: {{ health_check_url | default('https://staging.michaelschiemer.de/health') }}"
|
||||
- ""
|
||||
- "Next: Verify application is healthy"
|
||||
138
deployment/ansible/playbooks/fix-staging-compose-secrets.yml
Normal file
138
deployment/ansible/playbooks/fix-staging-compose-secrets.yml
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
- name: Fix Staging docker-compose.staging.yml with Redis Secrets
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
tasks:
|
||||
- name: Check current docker-compose.staging.yml on server
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== Current staging-app environment (REDIS-related) ==="
|
||||
grep -A 50 "staging-app:" docker-compose.staging.yml | grep -A 30 "environment:" | grep -E "(REDIS_|CACHE_|SESSION_)" || echo "No Redis env vars found"
|
||||
echo ""
|
||||
echo "=== Current secrets section for staging-app ==="
|
||||
grep -A 10 "staging-app:" docker-compose.staging.yml | grep -A 15 "secrets:" || echo "Secrets section not found"
|
||||
echo ""
|
||||
echo "=== Secrets definitions at bottom ==="
|
||||
tail -30 docker-compose.staging.yml | grep -A 10 "secrets:" || echo "Secrets definitions not found"
|
||||
register: current_config
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display current configuration
|
||||
debug:
|
||||
msg: "{{ current_config.stdout_lines }}"
|
||||
|
||||
- name: Get repository root path
|
||||
shell: |
|
||||
cd "{{ playbook_dir }}/../../.."
|
||||
pwd
|
||||
register: repo_root
|
||||
changed_when: false
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
|
||||
- name: Display repository root
|
||||
debug:
|
||||
msg: "Repository root: {{ repo_root.stdout }}"
|
||||
|
||||
- name: Check if docker-compose.staging.yml exists in repository
|
||||
stat:
|
||||
path: "{{ repo_root.stdout }}/docker-compose.staging.yml"
|
||||
register: compose_file_stat
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
|
||||
- name: Read docker-compose.staging.yml from repository
|
||||
slurp:
|
||||
src: "{{ repo_root.stdout }}/docker-compose.staging.yml"
|
||||
register: compose_file_content
|
||||
when: compose_file_stat.stat.exists
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
|
||||
- name: Write docker-compose.staging.yml to server
|
||||
copy:
|
||||
content: "{{ compose_file_content.content | b64decode }}"
|
||||
dest: "~/deployment/stacks/staging/docker-compose.staging.yml"
|
||||
mode: '0644'
|
||||
when: compose_file_stat.stat.exists
|
||||
|
||||
- name: Fail if docker-compose.staging.yml not found
|
||||
fail:
|
||||
msg: "Could not find docker-compose.staging.yml at {{ repo_root.stdout }}/docker-compose.staging.yml. Please ensure the file exists in the repository root."
|
||||
when: not compose_file_stat.stat.exists
|
||||
|
||||
- name: Verify updated docker-compose.staging.yml on server
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== Updated staging-app environment (REDIS-related) ==="
|
||||
grep -A 50 "staging-app:" docker-compose.staging.yml | grep -A 30 "environment:" | grep -E "(REDIS_|CACHE_|SESSION_|_FILE)" || echo "No Redis env vars found"
|
||||
echo ""
|
||||
echo "=== Updated secrets section for staging-app ==="
|
||||
grep -A 10 "staging-app:" docker-compose.staging.yml | grep -A 15 "secrets:" || echo "Secrets section not found"
|
||||
echo ""
|
||||
echo "=== Secrets definitions at bottom ==="
|
||||
tail -30 docker-compose.staging.yml | grep -A 10 "redis_password:" || echo "Secrets definitions not found"
|
||||
register: updated_config
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display updated configuration
|
||||
debug:
|
||||
msg: "{{ updated_config.stdout_lines }}"
|
||||
|
||||
- name: Restart staging containers to apply changes
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d --force-recreate
|
||||
register: restart_result
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display restart result
|
||||
debug:
|
||||
msg: "{{ restart_result.stdout_lines }}"
|
||||
|
||||
- name: Wait for containers to start
|
||||
pause:
|
||||
seconds: 10
|
||||
|
||||
- name: Check container status after fix
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
|
||||
register: container_status
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display container status
|
||||
debug:
|
||||
msg: "{{ container_status.stdout_lines }}"
|
||||
|
||||
- name: Verify REDIS_PASSWORD_FILE in container
|
||||
shell: |
|
||||
echo "=== Checking REDIS_PASSWORD_FILE in staging-app container ==="
|
||||
docker exec staging-app env | grep REDIS_PASSWORD || echo "REDIS_PASSWORD variables not found"
|
||||
docker exec staging-app env | grep "_FILE" | grep REDIS || echo "REDIS_PASSWORD_FILE not found"
|
||||
echo ""
|
||||
echo "=== Checking /run/secrets/redis_password ==="
|
||||
docker exec staging-app ls -la /run/secrets/redis_password 2>&1 || echo "Secret file not found"
|
||||
register: container_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display container verification
|
||||
debug:
|
||||
msg: "{{ container_check.stdout_lines }}"
|
||||
|
||||
- name: Summary
|
||||
debug:
|
||||
msg:
|
||||
- "========================================"
|
||||
- "FIX SUMMARY"
|
||||
- "========================================"
|
||||
- "1. Updated docker-compose.staging.yml on server"
|
||||
- "2. Restarted staging containers"
|
||||
- "3. Verified REDIS_PASSWORD_FILE configuration"
|
||||
- ""
|
||||
- "Next steps:"
|
||||
- "- Check staging-app logs: docker logs staging-app"
|
||||
- "- Test Redis connection from staging-app container"
|
||||
- "- Verify no more NOAUTH errors in logs"
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
- name: Fix Staging Secrets Permissions
|
||||
hosts: production
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
tasks:
|
||||
- name: Check secrets file permissions in staging-app container
|
||||
shell: |
|
||||
echo "=== Checking /run/secrets/redis_password permissions ==="
|
||||
docker exec staging-app ls -la /run/secrets/redis_password 2>&1 || echo "File not found"
|
||||
echo ""
|
||||
echo "=== Checking /run/secrets directory permissions ==="
|
||||
docker exec staging-app ls -la /run/secrets/ | head -10
|
||||
echo ""
|
||||
echo "=== Current user ==="
|
||||
docker exec staging-app whoami
|
||||
echo ""
|
||||
echo "=== Testing file read access ==="
|
||||
docker exec staging-app cat /run/secrets/redis_password 2>&1 | head -c 20 || echo "Cannot read file"
|
||||
echo "..."
|
||||
register: permissions_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display permissions check
|
||||
debug:
|
||||
msg: "{{ permissions_check.stdout_lines }}"
|
||||
|
||||
- name: Try to fix permissions via entrypoint modification
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
# Check if staging-app has an entrypoint that can be modified
|
||||
grep -A 5 "staging-app:" docker-compose.staging.yml | grep -A 10 "entrypoint:" | head -5
|
||||
register: entrypoint_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display entrypoint check
|
||||
debug:
|
||||
msg: "{{ entrypoint_check.stdout_lines }}"
|
||||
|
||||
- name: Check if we can read secrets as root in container
|
||||
shell: |
|
||||
echo "=== Reading secret as root ==="
|
||||
docker exec -u root staging-app cat /run/secrets/redis_password 2>&1 | head -c 20 || echo "Cannot read even as root"
|
||||
echo "..."
|
||||
echo ""
|
||||
echo "=== Checking file owner ==="
|
||||
docker exec -u root staging-app stat -c "%U:%G %a" /run/secrets/redis_password 2>&1 || echo "Cannot stat"
|
||||
register: root_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display root check
|
||||
debug: "{{ root_check.stdout_lines }}"
|
||||
debug:
|
||||
msg: "{{ root_check.stdout_lines }}"
|
||||
|
||||
- name: Check container user configuration
|
||||
shell: |
|
||||
cd ~/deployment/stacks/staging
|
||||
echo "=== staging-app user configuration ==="
|
||||
grep -A 20 "staging-app:" docker-compose.staging.yml | grep -E "(user:|USER)" || echo "No user specified (defaults to www-data)"
|
||||
register: user_config
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display user configuration
|
||||
debug:
|
||||
msg: "{{ user_config.stdout_lines }}"
|
||||
|
||||
- name: Summary and Recommendations
|
||||
debug:
|
||||
msg:
|
||||
- "========================================"
|
||||
- "PERMISSIONS ISSUE ANALYSIS"
|
||||
- "========================================"
|
||||
- "The secret file exists but is not readable by the PHP process."
|
||||
- ""
|
||||
- "Possible solutions:"
|
||||
- "1. Run PHP-FPM as root (NOT RECOMMENDED for security)"
|
||||
- "2. Create a wrapper script that reads secrets as root and exports them"
|
||||
- "3. Modify entrypoint to chmod/chown secrets (may not work on /run/secrets)"
|
||||
- "4. Use environment variables instead of file-based secrets"
|
||||
- "5. Modify docker-compose to use a different secrets mount path with proper permissions"
|
||||
Reference in New Issue
Block a user