feat: Integrate Ansible playbooks into CI/CD workflows
- Add deploy-application-code.yml for Git-based code deployment - Add install-composer-dependencies.yml for dependency installation - Add deploy-image.yml for Docker image deployment - Update build-image.yml to use Ansible playbooks - Update manual-deploy.yml to use Ansible playbooks - Add ANSIBLE_VAULT_PASSWORD secret handling
This commit is contained in:
@@ -963,169 +963,57 @@ jobs:
|
||||
chmod 600 ~/.ssh/production
|
||||
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy to Staging Server
|
||||
- name: Install Ansible
|
||||
run: |
|
||||
set -e
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ansible python3-pip
|
||||
pip3 install --user ansible-core docker
|
||||
|
||||
DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}"
|
||||
REGISTRY_HOST="${{ env.REGISTRY }}"
|
||||
IMAGE_NAME="${{ env.IMAGE_NAME }}"
|
||||
|
||||
DEFAULT_IMAGE="${REGISTRY_HOST}/${IMAGE_NAME}:latest"
|
||||
|
||||
# Always use latest image - if a build happened, it would have pushed to latest anyway
|
||||
# Using latest ensures we always get the most recent image, whether it was just built or not
|
||||
SELECTED_IMAGE="$DEFAULT_IMAGE"
|
||||
|
||||
STACK_PATH_DISPLAY="~/deployment/stacks/staging"
|
||||
|
||||
SELECTED_TAG="${SELECTED_IMAGE##*:}"
|
||||
SELECTED_REPO="${SELECTED_IMAGE%:*}"
|
||||
|
||||
if [ -z "$SELECTED_REPO" ] || [ "$SELECTED_REPO" = "$SELECTED_IMAGE" ]; then
|
||||
FALLBACK_IMAGE="$DEFAULT_IMAGE"
|
||||
- name: Create Ansible Vault password file
|
||||
run: |
|
||||
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
|
||||
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
echo "✅ Vault password file created"
|
||||
else
|
||||
FALLBACK_IMAGE="${SELECTED_REPO}:latest"
|
||||
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
|
||||
touch /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
fi
|
||||
|
||||
echo "🚀 Starting staging deployment..."
|
||||
echo " Image: ${SELECTED_IMAGE}"
|
||||
echo " Tag: ${SELECTED_TAG}"
|
||||
echo " Host: ${DEPLOYMENT_HOST}"
|
||||
echo " Stack: ${STACK_PATH_DISPLAY}"
|
||||
- name: Deploy Application Code to Staging
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-application-code.yml \
|
||||
-e "deployment_environment=staging" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "git_branch=staging" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
FULL_IMAGE_ARG=$(printf '%q' "$SELECTED_IMAGE")
|
||||
FALLBACK_IMAGE_ARG=$(printf '%q' "$FALLBACK_IMAGE")
|
||||
IMAGE_NAME_ARG=$(printf '%q' "$IMAGE_NAME")
|
||||
REGISTRY_ARG=$(printf '%q' "$REGISTRY_HOST")
|
||||
- name: Install Composer Dependencies
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/install-composer-dependencies.yml \
|
||||
-e "deployment_environment=staging" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
ssh -i ~/.ssh/production \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
deploy@${DEPLOYMENT_HOST} "bash -s -- $FULL_IMAGE_ARG $FALLBACK_IMAGE_ARG $IMAGE_NAME_ARG $REGISTRY_ARG" <<'EOF'
|
||||
set -e
|
||||
|
||||
FULL_IMAGE="$1"
|
||||
FALLBACK_IMAGE="$2"
|
||||
IMAGE_NAME="$3"
|
||||
REGISTRY="$4"
|
||||
shift 4
|
||||
|
||||
CURRENT_USER="$(whoami)"
|
||||
USER_HOME="$(getent passwd "$CURRENT_USER" | cut -d: -f6 2>/dev/null)"
|
||||
[ -z "$USER_HOME" ] && USER_HOME="$HOME"
|
||||
[ -z "$USER_HOME" ] && USER_HOME="/home/$CURRENT_USER"
|
||||
|
||||
STACK_TARGET="${USER_HOME}/deployment/stacks/staging"
|
||||
|
||||
# Ensure staging stack directory exists
|
||||
mkdir -p "${STACK_TARGET}"
|
||||
cd "${STACK_TARGET}"
|
||||
|
||||
declare -a REGISTRY_TARGETS=()
|
||||
if [ -n "${REGISTRY}" ]; then
|
||||
REGISTRY_TARGETS+=("${REGISTRY}")
|
||||
fi
|
||||
for IMAGE_REF in "${FULL_IMAGE}" "${FALLBACK_IMAGE}"; do
|
||||
if [ -n "${IMAGE_REF}" ]; then
|
||||
HOST_PART="${IMAGE_REF%%/*}"
|
||||
if [ -n "${HOST_PART}" ]; then
|
||||
if ! printf '%s\n' "${REGISTRY_TARGETS[@]}" | grep -qx "${HOST_PART}"; then
|
||||
REGISTRY_TARGETS+=("${HOST_PART}")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
for TARGET in "${REGISTRY_TARGETS[@]}"; do
|
||||
[ -z "${TARGET}" ] && continue
|
||||
echo "🔐 Logging in to Docker registry ${TARGET}..."
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${TARGET}" \
|
||||
-u "${{ secrets.REGISTRY_USER }}" \
|
||||
--password-stdin || echo "⚠️ Registry login failed for ${TARGET}, continuing..."
|
||||
done
|
||||
|
||||
DEPLOY_IMAGE="$FULL_IMAGE"
|
||||
echo "📥 Pulling image ${DEPLOY_IMAGE}..."
|
||||
if ! docker pull "${DEPLOY_IMAGE}"; then
|
||||
if [ -n "${FALLBACK_IMAGE}" ] && [ "${DEPLOY_IMAGE}" != "${FALLBACK_IMAGE}" ]; then
|
||||
echo "⚠️ Failed to pull ${DEPLOY_IMAGE}, attempting fallback ${FALLBACK_IMAGE}"
|
||||
if docker pull "${FALLBACK_IMAGE}"; then
|
||||
DEPLOY_IMAGE="${FALLBACK_IMAGE}"
|
||||
echo "ℹ️ Using fallback image ${DEPLOY_IMAGE}"
|
||||
else
|
||||
echo "❌ Failed to pull fallback image ${FALLBACK_IMAGE}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to pull image ${DEPLOY_IMAGE}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy base and staging docker-compose files if they don't exist
|
||||
if [ ! -f docker-compose.base.yml ]; then
|
||||
echo "⚠️ docker-compose.base.yml not found, copying from repo..."
|
||||
cp /workspace/repo/docker-compose.base.yml . || {
|
||||
echo "❌ Failed to copy docker-compose.base.yml"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ ! -f docker-compose.staging.yml ]; then
|
||||
echo "⚠️ docker-compose.staging.yml not found, copying from repo..."
|
||||
cp /workspace/repo/docker-compose.staging.yml . || {
|
||||
echo "❌ Failed to copy docker-compose.staging.yml"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Update docker-compose.staging.yml with new image tag
|
||||
echo "📝 Updating docker-compose.staging.yml with new image tag..."
|
||||
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${DEPLOY_IMAGE}|g" docker-compose.staging.yml
|
||||
|
||||
echo "✅ Updated docker-compose.staging.yml:"
|
||||
grep "image:" docker-compose.staging.yml | head -5
|
||||
|
||||
# Ensure networks exist
|
||||
echo "🔗 Ensuring Docker networks exist..."
|
||||
docker network create traefik-public 2>/dev/null || true
|
||||
docker network create staging-internal 2>/dev/null || true
|
||||
|
||||
echo "🔄 Starting/updating services..."
|
||||
# Use --pull missing instead of --pull always since we already pulled the specific image
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d --pull missing --force-recreate || {
|
||||
echo "❌ Failed to start services"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "⏳ Waiting for services to start..."
|
||||
sleep 15
|
||||
|
||||
# Pull latest code from Git repository - always sync code when deploying
|
||||
echo "🔄 Pulling latest code from Git repository in staging-app container..."
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-app bash -c "cd /var/www/html && git -c safe.directory=/var/www/html fetch origin staging && git -c safe.directory=/var/www/html reset --hard origin/staging && git -c safe.directory=/var/www/html clean -fd" || echo "⚠️ Git pull failed, container will sync on next restart"
|
||||
|
||||
# Also trigger a restart to ensure entrypoint script runs
|
||||
echo "🔄 Restarting staging-app to ensure all services are up-to-date..."
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml restart staging-app || echo "⚠️ Failed to restart staging-app"
|
||||
|
||||
# Fix nginx upstream configuration - critical fix for 502 errors
|
||||
# sites-available/default uses 127.0.0.1:9000 but PHP-FPM runs in staging-app container
|
||||
echo "🔧 Fixing nginx PHP-FPM upstream configuration (post-deploy fix)..."
|
||||
sleep 5
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx sed -i '/upstream php-upstream {/,/}/s|server 127.0.0.1:9000;|server staging-app:9000;|g' /etc/nginx/sites-available/default || echo "⚠️ Upstream fix (127.0.0.1) failed"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx sed -i '/upstream php-upstream {/,/}/s|server localhost:9000;|server staging-app:9000;|g' /etc/nginx/sites-available/default || echo "⚠️ Upstream fix (localhost) failed"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx nginx -t && docker compose -f docker-compose.base.yml -f docker-compose.staging.yml restart staging-nginx || echo "⚠️ Nginx config test or restart failed"
|
||||
echo "✅ Nginx configuration fixed and reloaded"
|
||||
|
||||
echo "⏳ Waiting for services to stabilize..."
|
||||
sleep 10
|
||||
echo "📊 Container status:"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
|
||||
|
||||
echo "✅ Staging deployment completed!"
|
||||
EOF
|
||||
- name: Deploy Docker Image to Staging
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-image.yml \
|
||||
-e "deployment_environment=staging" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "image_tag=latest" \
|
||||
-e "docker_registry=${{ env.REGISTRY }}" \
|
||||
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
|
||||
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
- name: Wait for deployment to stabilize
|
||||
run: sleep 30
|
||||
@@ -1133,16 +1021,42 @@ jobs:
|
||||
- name: Health check
|
||||
id: health
|
||||
run: |
|
||||
echo "🔍 Performing health checks..."
|
||||
|
||||
# Basic health check
|
||||
BASIC_HEALTH_OK=false
|
||||
for i in {1..10}; do
|
||||
if curl -f -k https://staging.michaelschiemer.de/health; then
|
||||
echo "✅ Health check passed"
|
||||
exit 0
|
||||
if curl -f -k -s https://staging.michaelschiemer.de/health > /dev/null 2>&1; then
|
||||
echo "✅ Basic health check passed"
|
||||
BASIC_HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
echo "⏳ Waiting for staging service... (attempt $i/10)"
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed"
|
||||
|
||||
if [ "$BASIC_HEALTH_OK" != "true" ]; then
|
||||
echo "❌ Basic health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extended health check (if available)
|
||||
echo "🔍 Checking extended health status..."
|
||||
HEALTH_SUMMARY=$(curl -f -k -s https://staging.michaelschiemer.de/admin/health/api/summary 2>/dev/null || echo "")
|
||||
if [ -n "$HEALTH_SUMMARY" ]; then
|
||||
OVERALL_STATUS=$(echo "$HEALTH_SUMMARY" | grep -o '"overall_status":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
|
||||
echo "📊 Overall health status: $OVERALL_STATUS"
|
||||
if [ "$OVERALL_STATUS" = "unhealthy" ]; then
|
||||
echo "⚠️ Extended health check shows unhealthy status"
|
||||
echo " Health summary: $HEALTH_SUMMARY"
|
||||
else
|
||||
echo "✅ Extended health check passed"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Extended health check endpoint not available (this is OK)"
|
||||
fi
|
||||
|
||||
echo "✅ All health checks completed"
|
||||
|
||||
- name: Notify deployment success
|
||||
if: success()
|
||||
@@ -1204,14 +1118,27 @@ jobs:
|
||||
chmod 600 ~/.ssh/production
|
||||
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy to Production Server
|
||||
- name: Install Ansible
|
||||
run: |
|
||||
set -e
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ansible python3-pip
|
||||
pip3 install --user ansible-core docker
|
||||
|
||||
DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}"
|
||||
REGISTRY="${{ env.REGISTRY }}"
|
||||
IMAGE_NAME="${{ env.IMAGE_NAME }}"
|
||||
- name: Create Ansible Vault password file
|
||||
run: |
|
||||
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
|
||||
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
echo "✅ Vault password file created"
|
||||
else
|
||||
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
|
||||
touch /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
fi
|
||||
|
||||
- name: Determine image tag
|
||||
id: image_tag
|
||||
run: |
|
||||
# Get image tag from build job output with fallback
|
||||
IMAGE_TAG="${{ needs.build.outputs.image_tag }}"
|
||||
|
||||
@@ -1229,76 +1156,42 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
STACK_PATH="~/deployment/stacks/application"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT
|
||||
echo "📦 Image Tag: ${IMAGE_TAG}"
|
||||
|
||||
echo "🚀 Starting production deployment..."
|
||||
echo " Image: ${FULL_IMAGE}"
|
||||
echo " Tag: ${IMAGE_TAG}"
|
||||
echo " Host: ${DEPLOYMENT_HOST}"
|
||||
echo " Stack: ${STACK_PATH}"
|
||||
- name: Deploy Application Code to Production
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-application-code.yml \
|
||||
-e "deployment_environment=production" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "git_branch=main" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
echo "📋 Deploying docker-compose configuration files..."
|
||||
cd /workspace/repo
|
||||
scp -i ~/.ssh/production \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
docker-compose.base.yml docker-compose.production.yml \
|
||||
deploy@${DEPLOYMENT_HOST}:${STACK_PATH}/ || {
|
||||
echo "❌ Failed to deploy docker-compose files"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Docker Compose files deployed successfully"
|
||||
- name: Install Composer Dependencies
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/install-composer-dependencies.yml \
|
||||
-e "deployment_environment=production" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
ssh -i ~/.ssh/production \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
deploy@${DEPLOYMENT_HOST} <<EOF
|
||||
set -e
|
||||
|
||||
cd ${STACK_PATH}
|
||||
|
||||
echo "🔐 Logging in to Docker registry..."
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${REGISTRY} \
|
||||
-u "${{ secrets.REGISTRY_USER }}" \
|
||||
--password-stdin || echo "⚠️ Registry login failed, continuing..."
|
||||
|
||||
echo "📥 Pulling image ${FULL_IMAGE}..."
|
||||
docker pull ${FULL_IMAGE} || {
|
||||
echo "❌ Failed to pull image ${FULL_IMAGE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure docker-compose files exist (rsync deployment handles this)
|
||||
if [ ! -f docker-compose.base.yml ] || [ ! -f docker-compose.production.yml ]; then
|
||||
echo "❌ Docker Compose files not found in ${STACK_PATH}"
|
||||
echo " Expected files are deployed via rsync in deployment scripts"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Docker Compose files present in deployment directory"
|
||||
|
||||
echo "📝 Updating docker-compose.production.yml with new image tag..."
|
||||
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${FULL_IMAGE}|g" docker-compose.production.yml
|
||||
sed -i "s|image:.*/${IMAGE_NAME}@.*|image: ${FULL_IMAGE}|g" docker-compose.production.yml
|
||||
|
||||
echo "✅ Updated docker-compose.production.yml:"
|
||||
grep "image:" docker-compose.production.yml | head -5
|
||||
|
||||
echo "🔄 Restarting services..."
|
||||
# Use --pull missing instead of --pull always since we already pulled the specific image
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.production.yml up -d --pull missing --force-recreate || {
|
||||
echo "❌ Failed to restart services"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "⏳ Waiting for services to start..."
|
||||
sleep 10
|
||||
|
||||
echo "📊 Container status:"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.production.yml ps
|
||||
|
||||
echo "✅ Production deployment completed!"
|
||||
EOF
|
||||
- name: Deploy Docker Image to Production
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-image.yml \
|
||||
-e "deployment_environment=production" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "image_tag=${{ steps.image_tag.outputs.IMAGE_TAG }}" \
|
||||
-e "docker_registry=${{ env.REGISTRY }}" \
|
||||
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
|
||||
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
- name: Wait for deployment to stabilize
|
||||
run: sleep 30
|
||||
@@ -1306,16 +1199,42 @@ jobs:
|
||||
- name: Health check
|
||||
id: health
|
||||
run: |
|
||||
echo "🔍 Performing health checks..."
|
||||
|
||||
# Basic health check
|
||||
BASIC_HEALTH_OK=false
|
||||
for i in {1..10}; do
|
||||
if curl -f -k https://michaelschiemer.de/health; then
|
||||
echo "✅ Health check passed"
|
||||
exit 0
|
||||
if curl -f -k -s https://michaelschiemer.de/health > /dev/null 2>&1; then
|
||||
echo "✅ Basic health check passed"
|
||||
BASIC_HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
echo "⏳ Waiting for production service... (attempt $i/10)"
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed"
|
||||
|
||||
if [ "$BASIC_HEALTH_OK" != "true" ]; then
|
||||
echo "❌ Basic health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extended health check (if available)
|
||||
echo "🔍 Checking extended health status..."
|
||||
HEALTH_SUMMARY=$(curl -f -k -s https://michaelschiemer.de/admin/health/api/summary 2>/dev/null || echo "")
|
||||
if [ -n "$HEALTH_SUMMARY" ]; then
|
||||
OVERALL_STATUS=$(echo "$HEALTH_SUMMARY" | grep -o '"overall_status":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
|
||||
echo "📊 Overall health status: $OVERALL_STATUS"
|
||||
if [ "$OVERALL_STATUS" = "unhealthy" ]; then
|
||||
echo "⚠️ Extended health check shows unhealthy status"
|
||||
echo " Health summary: $HEALTH_SUMMARY"
|
||||
else
|
||||
echo "✅ Extended health check passed"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Extended health check endpoint not available (this is OK)"
|
||||
fi
|
||||
|
||||
echo "✅ All health checks completed"
|
||||
|
||||
- name: Notify deployment success
|
||||
if: success()
|
||||
|
||||
@@ -112,173 +112,57 @@ jobs:
|
||||
chmod 600 ~/.ssh/production
|
||||
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy to Staging Server
|
||||
- name: Install Ansible
|
||||
run: |
|
||||
set -e
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ansible python3-pip
|
||||
pip3 install --user ansible-core docker
|
||||
|
||||
DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}"
|
||||
REGISTRY_HOST="${{ needs.determine-image.outputs.registry_host }}"
|
||||
IMAGE_NAME="${{ needs.determine-image.outputs.image_name }}"
|
||||
DEPLOY_IMAGE="${{ needs.determine-image.outputs.image_url }}"
|
||||
IMAGE_TAG="${{ needs.determine-image.outputs.image_tag }}"
|
||||
|
||||
DEFAULT_IMAGE="${REGISTRY_HOST}/${IMAGE_NAME}:latest"
|
||||
FALLBACK_IMAGE="$DEFAULT_IMAGE"
|
||||
|
||||
SELECTED_IMAGE="$DEPLOY_IMAGE"
|
||||
if [ -z "$SELECTED_IMAGE" ] || [ "$SELECTED_IMAGE" = "null" ]; then
|
||||
SELECTED_IMAGE="$DEFAULT_IMAGE"
|
||||
fi
|
||||
|
||||
STACK_PATH_DISPLAY="~/deployment/stacks/staging"
|
||||
|
||||
SELECTED_TAG="${SELECTED_IMAGE##*:}"
|
||||
SELECTED_REPO="${SELECTED_IMAGE%:*}"
|
||||
|
||||
if [ -z "$SELECTED_REPO" ] || [ "$SELECTED_REPO" = "$SELECTED_IMAGE" ]; then
|
||||
FALLBACK_IMAGE="$DEFAULT_IMAGE"
|
||||
- name: Create Ansible Vault password file
|
||||
run: |
|
||||
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
|
||||
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
echo "✅ Vault password file created"
|
||||
else
|
||||
FALLBACK_IMAGE="${SELECTED_REPO}:latest"
|
||||
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
|
||||
touch /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
fi
|
||||
|
||||
echo "🚀 Starting staging deployment..."
|
||||
echo " Image: ${SELECTED_IMAGE}"
|
||||
echo " Tag: ${SELECTED_TAG}"
|
||||
echo " Host: ${DEPLOYMENT_HOST}"
|
||||
echo " Stack: ${STACK_PATH_DISPLAY}"
|
||||
- name: Deploy Application Code to Staging
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-application-code.yml \
|
||||
-e "deployment_environment=staging" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "git_branch=${{ steps.branch.outputs.BRANCH }}" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
FULL_IMAGE_ARG=$(printf '%q' "$SELECTED_IMAGE")
|
||||
FALLBACK_IMAGE_ARG=$(printf '%q' "$FALLBACK_IMAGE")
|
||||
IMAGE_NAME_ARG=$(printf '%q' "$IMAGE_NAME")
|
||||
REGISTRY_ARG=$(printf '%q' "$REGISTRY_HOST")
|
||||
- name: Install Composer Dependencies
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/install-composer-dependencies.yml \
|
||||
-e "deployment_environment=staging" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
ssh -i ~/.ssh/production \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
deploy@${DEPLOYMENT_HOST} "bash -s -- $FULL_IMAGE_ARG $FALLBACK_IMAGE_ARG $IMAGE_NAME_ARG $REGISTRY_ARG" <<'EOF'
|
||||
set -e
|
||||
|
||||
FULL_IMAGE="$1"
|
||||
FALLBACK_IMAGE="$2"
|
||||
IMAGE_NAME="$3"
|
||||
REGISTRY="$4"
|
||||
shift 4
|
||||
|
||||
CURRENT_USER="$(whoami)"
|
||||
USER_HOME="$(getent passwd "$CURRENT_USER" | cut -d: -f6 2>/dev/null)"
|
||||
[ -z "$USER_HOME" ] && USER_HOME="$HOME"
|
||||
[ -z "$USER_HOME" ] && USER_HOME="/home/$CURRENT_USER"
|
||||
|
||||
STACK_TARGET="${USER_HOME}/deployment/stacks/staging"
|
||||
|
||||
# Ensure staging stack directory exists
|
||||
mkdir -p "${STACK_TARGET}"
|
||||
cd "${STACK_TARGET}"
|
||||
|
||||
declare -a REGISTRY_TARGETS=()
|
||||
if [ -n "${REGISTRY}" ]; then
|
||||
REGISTRY_TARGETS+=("${REGISTRY}")
|
||||
fi
|
||||
for IMAGE_REF in "${FULL_IMAGE}" "${FALLBACK_IMAGE}"; do
|
||||
if [ -n "${IMAGE_REF}" ]; then
|
||||
HOST_PART="${IMAGE_REF%%/*}"
|
||||
if [ -n "${HOST_PART}" ]; then
|
||||
if ! printf '%s\n' "${REGISTRY_TARGETS[@]}" | grep -qx "${HOST_PART}"; then
|
||||
REGISTRY_TARGETS+=("${HOST_PART}")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
for TARGET in "${REGISTRY_TARGETS[@]}"; do
|
||||
[ -z "${TARGET}" ] && continue
|
||||
echo "🔐 Logging in to Docker registry ${TARGET}..."
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${TARGET}" \
|
||||
-u "${{ secrets.REGISTRY_USER }}" \
|
||||
--password-stdin || echo "⚠️ Registry login failed for ${TARGET}, continuing..."
|
||||
done
|
||||
|
||||
DEPLOY_IMAGE="$FULL_IMAGE"
|
||||
echo "📥 Pulling image ${DEPLOY_IMAGE}..."
|
||||
if ! docker pull "${DEPLOY_IMAGE}"; then
|
||||
if [ -n "${FALLBACK_IMAGE}" ] && [ "${DEPLOY_IMAGE}" != "${FALLBACK_IMAGE}" ]; then
|
||||
echo "⚠️ Failed to pull ${DEPLOY_IMAGE}, attempting fallback ${FALLBACK_IMAGE}"
|
||||
if docker pull "${FALLBACK_IMAGE}"; then
|
||||
DEPLOY_IMAGE="${FALLBACK_IMAGE}"
|
||||
echo "ℹ️ Using fallback image ${DEPLOY_IMAGE}"
|
||||
else
|
||||
echo "❌ Failed to pull fallback image ${FALLBACK_IMAGE}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to pull image ${DEPLOY_IMAGE}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy base and staging docker-compose files if they don't exist
|
||||
if [ ! -f docker-compose.base.yml ]; then
|
||||
echo "⚠️ docker-compose.base.yml not found, copying from repo..."
|
||||
cp /workspace/repo/docker-compose.base.yml . || {
|
||||
echo "❌ Failed to copy docker-compose.base.yml"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ ! -f docker-compose.staging.yml ]; then
|
||||
echo "⚠️ docker-compose.staging.yml not found, copying from repo..."
|
||||
cp /workspace/repo/docker-compose.staging.yml . || {
|
||||
echo "❌ Failed to copy docker-compose.staging.yml"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Update docker-compose.staging.yml with new image tag
|
||||
echo "📝 Updating docker-compose.staging.yml with new image tag..."
|
||||
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${DEPLOY_IMAGE}|g" docker-compose.staging.yml
|
||||
|
||||
echo "✅ Updated docker-compose.staging.yml:"
|
||||
grep "image:" docker-compose.staging.yml | head -5
|
||||
|
||||
# Ensure networks exist
|
||||
echo "🔗 Ensuring Docker networks exist..."
|
||||
docker network create traefik-public 2>/dev/null || true
|
||||
docker network create staging-internal 2>/dev/null || true
|
||||
|
||||
echo "🔄 Starting/updating services..."
|
||||
# Use --pull missing instead of --pull always since we already pulled the specific image
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d --pull missing --force-recreate || {
|
||||
echo "❌ Failed to start services"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "⏳ Waiting for services to start..."
|
||||
sleep 15
|
||||
|
||||
# Pull latest code from Git repository
|
||||
echo "🔄 Pulling latest code from Git repository in staging-app container..."
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-app bash -c "cd /var/www/html && git -c safe.directory=/var/www/html fetch origin staging && git -c safe.directory=/var/www/html reset --hard origin/staging && git -c safe.directory=/var/www/html clean -fd" || echo "⚠️ Git pull failed, container will sync on next restart"
|
||||
|
||||
# Also trigger a restart to ensure entrypoint script runs
|
||||
echo "🔄 Restarting staging-app to ensure all services are up-to-date..."
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml restart staging-app || echo "⚠️ Failed to restart staging-app"
|
||||
|
||||
# Fix nginx upstream configuration - critical fix for 502 errors
|
||||
# sites-available/default uses 127.0.0.1:9000 but PHP-FPM runs in staging-app container
|
||||
echo "🔧 Fixing nginx PHP-FPM upstream configuration (post-deploy fix)..."
|
||||
sleep 5
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx sed -i '/upstream php-upstream {/,/}/s|server 127.0.0.1:9000;|server staging-app:9000;|g' /etc/nginx/sites-available/default || echo "⚠️ Upstream fix (127.0.0.1) failed"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx sed -i '/upstream php-upstream {/,/}/s|server localhost:9000;|server staging-app:9000;|g' /etc/nginx/sites-available/default || echo "⚠️ Upstream fix (localhost) failed"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx nginx -t && docker compose -f docker-compose.base.yml -f docker-compose.staging.yml restart staging-nginx || echo "⚠️ Nginx config test or restart failed"
|
||||
echo "✅ Nginx configuration fixed and reloaded"
|
||||
|
||||
echo "⏳ Waiting for services to stabilize..."
|
||||
sleep 10
|
||||
echo "📊 Container status:"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
|
||||
|
||||
echo "✅ Staging deployment completed!"
|
||||
EOF
|
||||
- name: Deploy Docker Image to Staging
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-image.yml \
|
||||
-e "deployment_environment=staging" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "image_tag=${{ needs.determine-image.outputs.image_tag }}" \
|
||||
-e "docker_registry=${{ needs.determine-image.outputs.registry_host }}" \
|
||||
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
|
||||
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
- name: Wait for deployment to stabilize
|
||||
run: sleep 30
|
||||
@@ -353,82 +237,57 @@ jobs:
|
||||
chmod 600 ~/.ssh/production
|
||||
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy to Production Server
|
||||
- name: Install Ansible
|
||||
run: |
|
||||
set -e
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ansible python3-pip
|
||||
pip3 install --user ansible-core docker
|
||||
|
||||
DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}"
|
||||
REGISTRY="${{ needs.determine-image.outputs.registry_host }}"
|
||||
IMAGE_NAME="${{ needs.determine-image.outputs.image_name }}"
|
||||
IMAGE_TAG="${{ needs.determine-image.outputs.image_tag }}"
|
||||
|
||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
STACK_PATH="~/deployment/stacks/application"
|
||||
|
||||
echo "🚀 Starting production deployment..."
|
||||
echo " Image: ${FULL_IMAGE}"
|
||||
echo " Tag: ${IMAGE_TAG}"
|
||||
echo " Host: ${DEPLOYMENT_HOST}"
|
||||
echo " Stack: ${STACK_PATH}"
|
||||
|
||||
ssh -i ~/.ssh/production \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
deploy@${DEPLOYMENT_HOST} <<EOF
|
||||
set -e
|
||||
|
||||
cd ${STACK_PATH}
|
||||
|
||||
echo "🔐 Logging in to Docker registry..."
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${REGISTRY} \
|
||||
-u "${{ secrets.REGISTRY_USER }}" \
|
||||
--password-stdin || echo "⚠️ Registry login failed, continuing..."
|
||||
|
||||
echo "📥 Pulling image ${FULL_IMAGE}..."
|
||||
docker pull ${FULL_IMAGE} || {
|
||||
echo "❌ Failed to pull image ${FULL_IMAGE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy base and production docker-compose files if they don't exist
|
||||
if [ ! -f docker-compose.base.yml ]; then
|
||||
echo "⚠️ docker-compose.base.yml not found, copying from repo..."
|
||||
cp /workspace/repo/docker-compose.base.yml . || {
|
||||
echo "❌ Failed to copy docker-compose.base.yml"
|
||||
exit 1
|
||||
}
|
||||
- name: Create Ansible Vault password file
|
||||
run: |
|
||||
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
|
||||
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
echo "✅ Vault password file created"
|
||||
else
|
||||
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
|
||||
touch /tmp/vault_pass
|
||||
chmod 600 /tmp/vault_pass
|
||||
fi
|
||||
|
||||
if [ ! -f docker-compose.production.yml ]; then
|
||||
echo "⚠️ docker-compose.production.yml not found, copying from repo..."
|
||||
cp /workspace/repo/docker-compose.production.yml . || {
|
||||
echo "❌ Failed to copy docker-compose.production.yml"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
- name: Deploy Application Code to Production
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-application-code.yml \
|
||||
-e "deployment_environment=production" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "git_branch=${{ steps.branch.outputs.BRANCH }}" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
echo "📝 Updating docker-compose.production.yml with new image tag..."
|
||||
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${FULL_IMAGE}|g" docker-compose.production.yml
|
||||
sed -i "s|image:.*/${IMAGE_NAME}@.*|image: ${FULL_IMAGE}|g" docker-compose.production.yml
|
||||
- name: Install Composer Dependencies
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/install-composer-dependencies.yml \
|
||||
-e "deployment_environment=production" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
echo "✅ Updated docker-compose.production.yml:"
|
||||
grep "image:" docker-compose.production.yml | head -5
|
||||
|
||||
echo "🔄 Restarting services..."
|
||||
# Use --pull missing instead of --pull always since we already pulled the specific image
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.production.yml up -d --pull missing --force-recreate || {
|
||||
echo "❌ Failed to restart services"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "⏳ Waiting for services to start..."
|
||||
sleep 10
|
||||
|
||||
echo "📊 Container status:"
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.production.yml ps
|
||||
|
||||
echo "✅ Production deployment completed!"
|
||||
EOF
|
||||
- name: Deploy Docker Image to Production
|
||||
run: |
|
||||
cd /workspace/repo/deployment/ansible
|
||||
ansible-playbook -i inventory/production.yml \
|
||||
playbooks/deploy-image.yml \
|
||||
-e "deployment_environment=production" \
|
||||
-e "deployment_hosts=production" \
|
||||
-e "image_tag=${{ needs.determine-image.outputs.image_tag }}" \
|
||||
-e "docker_registry=${{ needs.determine-image.outputs.registry_host }}" \
|
||||
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
|
||||
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
--vault-password-file /tmp/vault_pass \
|
||||
--private-key ~/.ssh/production
|
||||
|
||||
- name: Wait for deployment to stabilize
|
||||
run: sleep 30
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
# Production Deployment - Centralized Variables
|
||||
# These variables are used across all playbooks
|
||||
|
||||
# System Maintenance
|
||||
system_update_packages: true
|
||||
system_apt_upgrade: dist
|
||||
system_enable_unattended_upgrades: true
|
||||
system_enable_unattended_reboot: false
|
||||
system_unattended_reboot_time: "02:00"
|
||||
system_enable_unattended_timer: true
|
||||
system_enable_docker_prune: false
|
||||
|
||||
# Deployment Paths
|
||||
deploy_user_home: "/home/deploy"
|
||||
stacks_base_path: "{{ deploy_user_home }}/deployment/stacks"
|
||||
app_stack_path: "{{ stacks_base_path }}/application"
|
||||
backups_path: "{{ deploy_user_home }}/deployment/backups"
|
||||
|
||||
# Docker Registry
|
||||
docker_registry: "localhost:5000"
|
||||
docker_registry_url: "localhost:5000"
|
||||
docker_registry_external: "registry.michaelschiemer.de"
|
||||
docker_registry_username_default: "admin"
|
||||
# docker_registry_password_default should be set in vault as vault_docker_registry_password
|
||||
# If not using vault, override via -e docker_registry_password_default="your-password"
|
||||
docker_registry_password_default: ""
|
||||
registry_auth_path: "{{ stacks_base_path }}/registry/auth"
|
||||
|
||||
# Application Configuration
|
||||
app_name: "framework"
|
||||
app_domain: "michaelschiemer.de"
|
||||
app_image: "{{ docker_registry }}/{{ app_name }}"
|
||||
app_image_external: "{{ docker_registry_external }}/{{ app_name }}"
|
||||
|
||||
# Domain Configuration
|
||||
gitea_domain: "git.michaelschiemer.de"
|
||||
|
||||
# Email Configuration
|
||||
mail_from_address: "noreply@{{ app_domain }}"
|
||||
acme_email: "kontakt@{{ app_domain }}"
|
||||
|
||||
# SSL Certificate Domains
|
||||
ssl_domains:
|
||||
- "{{ gitea_domain }}"
|
||||
- "{{ app_domain }}"
|
||||
|
||||
# Health Check Configuration
|
||||
health_check_url: "https://{{ app_domain }}/health"
|
||||
health_check_retries: 10
|
||||
health_check_delay: 10
|
||||
|
||||
# Rollback Configuration
|
||||
max_rollback_versions: 5
|
||||
rollback_timeout: 300
|
||||
|
||||
# Wait Timeouts
|
||||
wait_timeout: 60
|
||||
|
||||
# Git Configuration (for sync-code.yml)
|
||||
git_repository_url_default: "https://{{ gitea_domain }}/michael/michaelschiemer.git"
|
||||
git_branch_default: "main"
|
||||
git_token: "{{ vault_git_token | default('') }}"
|
||||
git_username: "{{ vault_git_username | default('') }}"
|
||||
git_password: "{{ vault_git_password | default('') }}"
|
||||
|
||||
# Database Configuration
|
||||
db_user_default: "postgres"
|
||||
db_name_default: "michaelschiemer"
|
||||
|
||||
# MinIO Object Storage Configuration
|
||||
minio_root_user: "{{ vault_minio_root_user | default('minioadmin') }}"
|
||||
minio_root_password: "{{ vault_minio_root_password | default('') }}"
|
||||
minio_api_domain: "minio-api.michaelschiemer.de"
|
||||
minio_console_domain: "minio.michaelschiemer.de"
|
||||
|
||||
# WireGuard Configuration
|
||||
wireguard_interface: "wg0"
|
||||
wireguard_config_path: "/etc/wireguard"
|
||||
wireguard_port_default: 51820
|
||||
wireguard_network_default: "10.8.0.0/24"
|
||||
wireguard_server_ip_default: "10.8.0.1"
|
||||
wireguard_enable_ip_forwarding: true
|
||||
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
|
||||
wireguard_private_key_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}_private.key"
|
||||
wireguard_public_key_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}_public.key"
|
||||
wireguard_client_configs_path: "{{ wireguard_config_path }}/clients"
|
||||
|
||||
# WireGuard DNS Configuration
|
||||
# DNS server for VPN clients (points to VPN server IP)
|
||||
# This ensures internal services are resolved to VPN IPs
|
||||
wireguard_dns_servers:
|
||||
- "{{ wireguard_server_ip_default }}"
|
||||
123
deployment/ansible/playbooks/deploy-application-code.yml
Normal file
123
deployment/ansible/playbooks/deploy-application-code.yml
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
- name: Deploy Application Code via Git
|
||||
hosts: "{{ deployment_hosts | default('production') }}"
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
application_code_dest: "/home/deploy/michaelschiemer/current"
|
||||
git_repository_url: "{{ git_repository_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}"
|
||||
# Determine branch based on environment
|
||||
git_branch: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
{{ git_branch | default('staging') }}
|
||||
{%- else -%}
|
||||
{{ git_branch | default('main') }}
|
||||
{%- endif -%}
|
||||
git_token: "{{ git_token | default('') }}"
|
||||
# Deployment environment (staging or production)
|
||||
deployment_environment: "{{ deployment_environment | default('production') }}"
|
||||
|
||||
tasks:
|
||||
- name: Ensure Git is installed
|
||||
ansible.builtin.apt:
|
||||
name: git
|
||||
state: present
|
||||
update_cache: no
|
||||
become: yes
|
||||
|
||||
- name: Ensure application code directory exists
|
||||
file:
|
||||
path: "{{ application_code_dest }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
become: yes
|
||||
|
||||
- name: Check if repository already exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/.git"
|
||||
register: git_repo_exists
|
||||
|
||||
- name: Clone repository (if not exists)
|
||||
ansible.builtin.git:
|
||||
repo: "{{ git_repository_url }}"
|
||||
dest: "{{ application_code_dest }}"
|
||||
version: "{{ git_branch }}"
|
||||
force: no
|
||||
update: no
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
when: not git_repo_exists.stat.exists
|
||||
environment:
|
||||
GIT_TERMINAL_PROMPT: "0"
|
||||
vars:
|
||||
ansible_become: no
|
||||
|
||||
- name: Update repository (if exists)
|
||||
ansible.builtin.git:
|
||||
repo: "{{ git_repository_url }}"
|
||||
dest: "{{ application_code_dest }}"
|
||||
version: "{{ git_branch }}"
|
||||
force: yes
|
||||
update: yes
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
when: git_repo_exists.stat.exists
|
||||
environment:
|
||||
GIT_TERMINAL_PROMPT: "0"
|
||||
vars:
|
||||
ansible_become: no
|
||||
|
||||
- name: Ensure executable permissions on PHP scripts
|
||||
file:
|
||||
path: "{{ application_code_dest }}/{{ item }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- worker.php
|
||||
- console.php
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Verify worker.php exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/worker.php"
|
||||
register: worker_php_stat
|
||||
|
||||
- name: Verify console.php exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/console.php"
|
||||
register: console_php_stat
|
||||
|
||||
- name: Verify composer.json exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/composer.json"
|
||||
register: composer_json_stat
|
||||
|
||||
- name: Get current Git commit hash
|
||||
shell: |
|
||||
cd {{ application_code_dest }} && git rev-parse HEAD
|
||||
register: git_commit_hash
|
||||
changed_when: false
|
||||
when: git_repo_exists.stat.exists
|
||||
|
||||
- name: Display file verification results
|
||||
debug:
|
||||
msg: |
|
||||
File Verification:
|
||||
- worker.php: {{ 'EXISTS' if worker_php_stat.stat.exists else 'MISSING' }}
|
||||
- console.php: {{ 'EXISTS' if console_php_stat.stat.exists else 'MISSING' }}
|
||||
- composer.json: {{ 'EXISTS' if composer_json_stat.stat.exists else 'MISSING' }}
|
||||
- Git Branch: {{ git_branch }}
|
||||
- Git Commit: {{ git_commit_hash.stdout | default('N/A') }}
|
||||
|
||||
- name: Fail if critical files are missing
|
||||
fail:
|
||||
msg: |
|
||||
Critical files are missing after Git deployment:
|
||||
{% if not worker_php_stat.stat.exists %}- worker.php{% endif %}
|
||||
{% if not console_php_stat.stat.exists %}- console.php{% endif %}
|
||||
{% if not composer_json_stat.stat.exists %}- composer.json{% endif %}
|
||||
when:
|
||||
- not worker_php_stat.stat.exists or not console_php_stat.stat.exists or not composer_json_stat.stat.exists
|
||||
|
||||
142
deployment/ansible/playbooks/deploy-image.yml
Normal file
142
deployment/ansible/playbooks/deploy-image.yml
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
- name: Deploy Docker Image to Application Stack
|
||||
hosts: "{{ deployment_hosts | default('production') }}"
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# Determine stack path based on environment
|
||||
application_stack_dest: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
{{ staging_stack_path | default(stacks_base_path + '/staging') }}
|
||||
{%- else -%}
|
||||
{{ app_stack_path | default(stacks_base_path + '/production') }}
|
||||
{%- endif -%}
|
||||
application_compose_suffix: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
staging.yml
|
||||
{%- else -%}
|
||||
production.yml
|
||||
{%- endif -%}
|
||||
# Image to deploy (can be overridden via -e image_tag=...)
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
docker_registry: "{{ docker_registry | default('registry.michaelschiemer.de') }}"
|
||||
app_name: "{{ app_name | default('framework') }}"
|
||||
# Full image URL
|
||||
deploy_image: "{{ docker_registry }}/{{ app_name }}:{{ image_tag }}"
|
||||
# Deployment environment (staging or production)
|
||||
deployment_environment: "{{ deployment_environment | default('production') }}"
|
||||
|
||||
tasks:
|
||||
- name: Determine Docker registry password from vault or extra vars
|
||||
ansible.builtin.set_fact:
|
||||
registry_password: >-
|
||||
{%- if docker_registry_password is defined and docker_registry_password | string | trim != '' -%}
|
||||
{{ docker_registry_password }}
|
||||
{%- elif vault_docker_registry_password is defined and vault_docker_registry_password | string | trim != '' -%}
|
||||
{{ vault_docker_registry_password }}
|
||||
{%- else -%}
|
||||
{{ '' }}
|
||||
{%- endif -%}
|
||||
no_log: yes
|
||||
|
||||
- name: Check if registry is accessible
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ docker_registry }}/v2/"
|
||||
method: GET
|
||||
status_code: [200, 401]
|
||||
timeout: 5
|
||||
register: registry_check
|
||||
ignore_errors: yes
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
become: no
|
||||
|
||||
- name: Login to Docker registry
|
||||
community.docker.docker_login:
|
||||
registry_url: "{{ docker_registry }}"
|
||||
username: "{{ docker_registry_username | default('admin') }}"
|
||||
password: "{{ registry_password }}"
|
||||
when:
|
||||
- registry_password | string | trim != ''
|
||||
- registry_check.status | default(0) in [200, 401]
|
||||
no_log: yes
|
||||
ignore_errors: yes
|
||||
register: docker_login_result
|
||||
|
||||
- name: Pull Docker image
|
||||
community.docker.docker_image:
|
||||
name: "{{ deploy_image }}"
|
||||
source: pull
|
||||
pull: true
|
||||
register: image_pull_result
|
||||
failed_when: image_pull_result.failed | default(false)
|
||||
|
||||
- name: Verify image exists locally
|
||||
community.docker.docker_image_info:
|
||||
name: "{{ deploy_image }}"
|
||||
register: image_info
|
||||
failed_when: image_info.failed | default(false)
|
||||
|
||||
- name: Update docker-compose file with new image tag
|
||||
ansible.builtin.replace:
|
||||
path: "{{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }}"
|
||||
regexp: '^(\s+image:\s+)({{ docker_registry }}/{{ app_name }}:)(.*)$'
|
||||
replace: '\1\2{{ image_tag }}'
|
||||
register: compose_update_result
|
||||
failed_when: false
|
||||
changed_when: compose_update_result.changed | default(false)
|
||||
|
||||
- name: Update docker-compose file with new image (alternative pattern)
|
||||
ansible.builtin.replace:
|
||||
path: "{{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }}"
|
||||
regexp: 'image:\s+{{ docker_registry }}/{{ app_name }}:.*'
|
||||
replace: 'image: {{ deploy_image }}'
|
||||
register: compose_update_alt
|
||||
when: compose_update_result.changed == false
|
||||
failed_when: false
|
||||
changed_when: compose_update_alt.changed | default(false)
|
||||
|
||||
- name: Ensure Docker networks exist
|
||||
community.docker.docker_network:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop:
|
||||
- traefik-public
|
||||
- app-internal
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Deploy application stack with new image
|
||||
shell: |
|
||||
cd {{ application_stack_dest }}
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} up -d --pull missing --force-recreate --remove-orphans
|
||||
register: compose_deploy_result
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for containers to start
|
||||
ansible.builtin.pause:
|
||||
seconds: 15
|
||||
|
||||
- name: Check container status
|
||||
shell: |
|
||||
cd {{ application_stack_dest }}
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} ps
|
||||
register: container_status
|
||||
changed_when: false
|
||||
|
||||
- name: Display deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
========================================
|
||||
Image Deployment Summary
|
||||
========================================
|
||||
Image: {{ deploy_image }}
|
||||
Tag: {{ image_tag }}
|
||||
Environment: {{ deployment_environment }}
|
||||
Stack: {{ application_stack_dest }}
|
||||
Status: SUCCESS
|
||||
========================================
|
||||
|
||||
Container Status:
|
||||
{{ container_status.stdout | default('Not available') }}
|
||||
========================================
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
- name: Install Composer Dependencies in Application Container
|
||||
hosts: "{{ deployment_hosts | default('production') }}"
|
||||
gather_facts: no
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# Determine stack path based on environment
|
||||
application_stack_dest: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
{{ staging_stack_path | default(stacks_base_path + '/staging') }}
|
||||
{%- else -%}
|
||||
{{ app_stack_path | default(stacks_base_path + '/production') }}
|
||||
{%- endif -%}
|
||||
application_compose_suffix: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
staging.yml
|
||||
{%- else -%}
|
||||
production.yml
|
||||
{%- endif -%}
|
||||
# Deployment environment (staging or production)
|
||||
deployment_environment: "{{ deployment_environment | default('production') }}"
|
||||
# Service name (php for production, staging-app for staging)
|
||||
php_service_name: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
staging-app
|
||||
{%- else -%}
|
||||
php
|
||||
{%- endif -%}
|
||||
|
||||
tasks:
|
||||
- name: Check if composer.json exists
|
||||
stat:
|
||||
path: /home/deploy/michaelschiemer/current/composer.json
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
register: composer_json_exists
|
||||
|
||||
- name: Fail if composer.json is missing
|
||||
fail:
|
||||
msg: "composer.json not found at /home/deploy/michaelschiemer/current/composer.json"
|
||||
when: not composer_json_exists.stat.exists
|
||||
|
||||
- name: Install composer dependencies in PHP container
|
||||
shell: |
|
||||
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ php_service_name }} composer install --no-dev --optimize-autoloader --no-interaction
|
||||
register: composer_install
|
||||
changed_when: true
|
||||
failed_when: composer_install.rc != 0
|
||||
|
||||
- name: Restart queue-worker and scheduler to pick up vendor directory (production only)
|
||||
shell: |
|
||||
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} restart queue-worker scheduler
|
||||
register: restart_workers
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
when: deployment_environment == 'production'
|
||||
|
||||
- name: Display composer install output
|
||||
debug:
|
||||
msg: |
|
||||
Composer Install Output:
|
||||
{{ composer_install.stdout }}
|
||||
|
||||
- name: Verify vendor/autoload.php exists
|
||||
shell: |
|
||||
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ php_service_name }} test -f /var/www/html/vendor/autoload.php && echo "EXISTS" || echo "MISSING"
|
||||
register: autoload_check
|
||||
changed_when: false
|
||||
|
||||
- name: Display autoload verification
|
||||
debug:
|
||||
msg: "vendor/autoload.php: {{ autoload_check.stdout.strip() }}"
|
||||
|
||||
- name: Fail if autoload.php is missing
|
||||
fail:
|
||||
msg: "vendor/autoload.php was not created after composer install"
|
||||
when: "autoload_check.stdout.strip() != 'EXISTS'"
|
||||
|
||||
16
src/Framework/Console/Layout/Components/StatusType.php
Normal file
16
src/Framework/Console/Layout/Components/StatusType.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Console\Layout\Components;
|
||||
|
||||
/**
|
||||
* Status type enum
|
||||
*/
|
||||
enum StatusType: string
|
||||
{
|
||||
case SUCCESS = 'success';
|
||||
case ERROR = 'error';
|
||||
case WARNING = 'warning';
|
||||
case INFO = 'info';
|
||||
case PENDING = 'pending';
|
||||
case LOADING = 'loading';
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
interface ErrorHandlerInterface
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderer;
|
||||
|
||||
use App\Framework\ExceptionHandling\ErrorRenderer;
|
||||
|
||||
final class HtmlErrorRenderer implements ErrorRenderer
|
||||
{
|
||||
public function render(): void
|
||||
{
|
||||
echo '<html lang="en"><body><h1>500 Internal Server Error</h1></body></html>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
interface ShutdownHandlerInterface
|
||||
{
|
||||
public function handle(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user