Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions app-infrastructure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Инфраструктура и деплой на сервер

Этот проект содержит всю инфраструктурную логику, необходимую для запуска и обновления приложения в продакшн-среде

## Основные концепции
Проект использует стратегию **Blue-Green Deployment** для достижения **Zero Downtime**

## Как это работает
1. **GitHub Actions**: Пайплайн управляет процессом обновления
2. **Blue/Green переключение**:
- Система поддерживает два параллельных окружения (`blue` и `green`)
- Деплой происходит путем скачивания нового образа из GHCR и запуска окружения
- **Healthcheck**: Трафик переключается через HAProxy только после того, как новый контейнер проходит проверку здоровья
- **Rollback**: Автоматический возврат к предыдущему стабильному состоянию при неудачном деплое
3. **Безопасность**:
- Использование `GitOps` подхода: код приложения и логика деплоя разделены
- Секреты не хранятся в репозитории, а подтягиваются из защищенного файла `.env` на сервере

## Скрипты
- `deploy.sh`: Точка входа для ci/cd
- `_deploy-general.sh`: Логика деплоя, переключения трафика
- `_rollback-general.sh`: Механизм мгновенного отката при сбоях


### Как запустить

1. Нужны два независимых репозитория в GitHub
- repo1 (репозиторий приложения: исходный код, Dockerfile, CI/CD пайплайны)
- repo2 (инфраструктурный репозиторий: конфигурации production, Nginx, скрипты деплоя)

2. Создаем локальный runner в repo1 (email-validator\docker-compose.runner.yml) внутри Docker-контейнера с использованием образа `myoung34/github-runner`

3. Обязательно указать свой токен GitHub Action token docker-compose.runner.yml. То есть зарегестрировать runner

4. Перед push в проект email-validator нужно обязательно на локальной машине запустить Docker и в уже клонированном проекте email-validator запустить runner командой

```
docker compose docker-compose.runner.yml build -d
```
148 changes: 148 additions & 0 deletions app-infrastructure/docker-compose.production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
services:
gateway:
build: ./haproxy
container_name: gateway
restart: unless-stopped
ports:
- '80:80'
volumes:
- './haproxy/gateway.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro'
networks:
- app_network

db:
image: postgres:16-alpine
container_name: postgres_db
restart: always
shm_size: 256mb
environment:
DB_CONNECTION: ${DB_CONNECTION}
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-app} -d ${DB_NAME:-app_db}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app_network

rabbitmq:
image: rabbitmq:3.13-management-alpine
container_name: rabbitmq
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app_network

# BLUE СРЕДА
blue_nginx:
image: nginx:alpine
container_name: blue_nginx
volumes:
- './nginx/blue.conf:/etc/nginx/conf.d/default.conf:ro'
depends_on:
blue:
condition: service_healthy
networks:
- app_network

blue:
image: ghcr.io/jony2good/email-validator:latest
container_name: blue
restart: unless-stopped
env_file: .env
environment:
- COLOR=blue
- RABBITMQ_HOST=rabbitmq
healthcheck:
test: ["CMD-SHELL", "php -r \"exit(fsockopen('127.0.0.1',9000)?0:1);\""]
interval: 10s
timeout: 5s
retries: 3
networks:
- app_network

blue_consumer:
image: ghcr.io/jony2good/email-validator:latest
container_name: blue_consumer
restart: on-failure
env_file: .env
environment:
- COLOR=blue
- RABBITMQ_HOST=rabbitmq
command: php bin/consume.php
depends_on:
rabbitmq:
condition: service_healthy
networks:
- app_network

# GREEN СРЕДА
green_nginx:
image: nginx:alpine
container_name: green_nginx
volumes:
- './nginx/green.conf:/etc/nginx/conf.d/default.conf:ro'
depends_on:
green:
condition: service_healthy
networks:
- app_network

green:
image: ghcr.io/jony2good/email-validator:latest
container_name: green
restart: unless-stopped
env_file: .env
environment:
- COLOR=green
- RABBITMQ_HOST=rabbitmq
healthcheck:
test: ["CMD-SHELL", "php -r \"exit(fsockopen('127.0.0.1',9000)?0:1);\""]
interval: 10s
timeout: 5s
retries: 3
networks:
- app_network

green_consumer:
image: ghcr.io/jony2good/email-validator:latest
container_name: green_consumer
restart: on-failure
env_file: .env
environment:
- COLOR=green
- RABBITMQ_HOST=rabbitmq
command: php bin/consume.php
depends_on:
rabbitmq:
condition: service_healthy
networks:
- app_network

networks:
app_network:
driver: bridge

volumes:
postgres_data:
rabbitmq_data:
13 changes: 13 additions & 0 deletions app-infrastructure/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
APP_ENV=production

DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=email-validator
DB_USERNAME=root
DB_PASSWORD=secret

RABBITMQ_HOST=rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_USER=admin
RABBITMQ_PASSWORD=secret
5 changes: 5 additions & 0 deletions app-infrastructure/haproxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM haproxy:alpine
USER root
RUN apk add --no-cache socat
RUN mkdir /sock && chown -R haproxy:haproxy /sock
USER haproxy
38 changes: 38 additions & 0 deletions app-infrastructure/haproxy/gateway.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
global
stats socket /sock/admin.sock user haproxy group haproxy mode 660 level admin
log stdout format raw local0 info

defaults
mode http
log global
option httplog
timeout connect 5s
timeout http-request 10s
timeout queue 30s
timeout client 50s
timeout server 50s

resolvers docker_dns
nameserver dns1 127.0.0.11:53
resolve_retries 3
timeout resolve 1s
timeout retry 1s
hold other 30s
hold refused 30s
hold nx 30s
hold timeout 30s
hold valid 10s
hold obsolete 30s

frontend fe_main
bind :80
default_backend blue_green

backend blue_green
option httpchk GET /app/health
http-check expect status 200
balance roundrobin

default-server check resolvers docker_dns init-addr none inter 2s fall 3 rise 2
server blue blue_nginx:80
server green green_nginx:80
30 changes: 30 additions & 0 deletions app-infrastructure/nginx/blue.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
server {
listen 80;
server_name localhost;

root /app/web;

include /etc/nginx/mime.types;

resolver 127.0.0.11 valid=10s;

location / {
try_files $uri /index.php$is_args$args;
}

location ~ ^/index\.php(/|$) {
set $backend blue:9000;
fastcgi_pass $backend;

fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;

internal;
}

error_log /dev/stderr;
access_log /dev/stdout;
}
30 changes: 30 additions & 0 deletions app-infrastructure/nginx/green.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
server {
listen 80;
server_name localhost;

root /app/web;

include /etc/nginx/mime.types;

resolver 127.0.0.11 valid=10s;

location / {
try_files $uri /index.php$is_args$args;
}

location ~ ^/index\.php(/|$) {
set $backend green:9000;
fastcgi_pass $backend;

fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;

internal;
}

error_log /dev/stderr;
access_log /dev/stdout;
}
34 changes: 34 additions & 0 deletions app-infrastructure/scripts/_deploy-general.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
set -e

if [ -z "$CURRENT" ] || [ -z "$PREV" ]; then
echo "Ошибка: Не заданы переменные CURRENT или PREV"
exit 1
fi

cd "$DEPLOY_DIR"

docker compose -f docker-compose.production.yml pull $CURRENT ${CURRENT}_consumer ${CURRENT}_nginx

docker compose -f docker-compose.production.yml up -d $CURRENT ${CURRENT}_consumer ${CURRENT}_nginx

ATTEMPTS=0
MAX_ATTEMPTS=30

until [ "$(docker inspect -f '{{.State.Health.Status}}' $CURRENT)" == "healthy" ]; do
sleep 2
ATTEMPTS=$((ATTEMPTS+1))

if [ "$ATTEMPTS" -eq "$MAX_ATTEMPTS" ]; then
docker compose -f docker-compose.production.yml stop $CURRENT ${CURRENT}_consumer ${CURRENT}_nginx
exit 1
fi
done

docker exec gateway sh -c "echo \"set server blue_green/${CURRENT} state ready\" | socat stdio unix-connect:/sock/admin.sock"

docker exec gateway sh -c "echo \"set server blue_green/${PREV} state maint\" | socat stdio unix-connect:/sock/admin.sock" || true

sleep 5

docker compose -f docker-compose.production.yml stop $PREV ${PREV}_nginx ${PREV}_consumer || true
29 changes: 29 additions & 0 deletions app-infrastructure/scripts/_rollback-general.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash
set -e

if [ -z "$CURRENT" ] || [ -z "$PREV" ]; then
exit 1
fi

cd "$DEPLOY_DIR"

docker compose -f docker-compose.production.yml up -d $PREV ${PREV}_consumer ${PREV}_nginx

ATTEMPTS=0
until [ "$(docker inspect -f '{{.State.Health.Status}}' $PREV)" == "healthy" ]; do
sleep 2
ATTEMPTS=$((ATTEMPTS+1))
if [ "$ATTEMPTS" -eq 30 ]; then
echo "ФАТАЛЬНАЯ ОШИБКА: Предыдущая версия ($PREV) также не проходит Healthcheck!"
echo "Требуется ручное вмешательство."
exit 1
fi
done

docker exec gateway sh -c "echo \"set server blue_green/${PREV} state ready\" | socat stdio unix-connect:/sock/admin.sock"

sleep 5

docker exec gateway sh -c "echo \"set server blue_green/${CURRENT} state maint\" | socat stdio unix-connect:/sock/admin.sock"

docker compose -f docker-compose.production.yml stop $CURRENT ${CURRENT}_nginx ${CURRENT}_consumer
Loading