Skip to content
Merged
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
154 changes: 96 additions & 58 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -1,70 +1,108 @@
name: CD - Deploy to Server via SSH
name: CD - Deploy to GCE Instances

on:
workflow_dispatch: # ✅ 수동 실행 지원
workflow_run:
workflows: ["CI - Upload Compose & Traefik Files"] # CI 워크플로우 이름
types:
- completed
repository_dispatch:
types: [deploy-backend]
workflow_dispatch:
inputs:
image_tag:
description: "Docker image tag to deploy (default: latest)"
required: false
default: "latest"
target:
description: "Target instance (all/app/ocr/alert)"
required: false
default: "all"

concurrency:
group: cd-deploy
cancel-in-progress: false # 배포 중 취소 방지

jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: Deploy ${{ matrix.instance.name }}
runs-on: ubuntu-latest
environment: production

strategy:
max-parallel: 1
fail-fast: false
matrix:
instance:
- { name: app, compose: docker-compose.app.yml }
- { name: ocr, compose: docker-compose.ocr.yml }
- { name: alert, compose: docker-compose.alert.yml }

steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Check target filter
id: check
run: |
TARGET="${{ github.event.inputs.target || 'all' }}"
CURRENT="${{ matrix.instance.name }}"
if [ "$TARGET" != "all" ] && [ "$TARGET" != "$CURRENT" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "⏭️ Skipping $CURRENT (target: $TARGET)"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Connect & Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
- uses: actions/checkout@v4
if: steps.check.outputs.skip != 'true'

- name: Authenticate to Google Cloud
if: steps.check.outputs.skip != 'true'
uses: google-github-actions/auth@v2
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_PEM_KEY }}
script: |
export SECRET_KEY="${{ secrets.SECRET_KEY }}"
export DJANGO_SETTINGS_MODULE="${{ secrets.DJANGO_SETTINGS_MODULE }}"

export MYSQL_USER="${{ secrets.MYSQL_USER }}"
export MYSQL_PASSWORD="${{ secrets.MYSQL_PASSWORD }}"
export MYSQL_DATABASE="${{ secrets.MYSQL_DATABASE }}"
export MYSQL_ROOT_PASSWORD="${{ secrets.MYSQL_ROOT_PASSWORD }}"

export AWS_ACCESS_KEY="${{ secrets.AWS_ACCESS_KEY }}"
export AWS_SECRET_KEY="${{ secrets.AWS_SECRET_KEY }}"
export AWS_S3_BUCKET_NAME="${{ secrets.AWS_S3_BUCKET_NAME }}"
export AWS_S3_REGION="${{ secrets.AWS_S3_REGION }}"
export GOOGLE_APPLICATION_CREDENTIALS="${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}"

export RABBITMQ_USER="${{ secrets.RABBITMQ_USER }}"
export RABBITMQ_PASSWORD="${{ secrets.RABBITMQ_PASSWORD }}"
export DOCKER_USERNAME="${{ secrets.DOCKER_USERNAME }}"
export DOCKER_IMAGE_NAME="${{ secrets.DOCKER_IMAGE_NAME }}"
export DOCKER_CELERY_NAME="${{ secrets.DOCKER_CELERY_NAME }}"

export TRAEFIK_DASHBOARD_AUTH="${{ secrets.TRAEFIK_DASHBOARD_AUTH }}"
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
if: steps.check.outputs.skip != 'true'
uses: google-github-actions/setup-gcloud@v2

- name: Get instance name
if: steps.check.outputs.skip != 'true'
id: instance
run: |
ROLE="${{ matrix.instance.name }}"
case "$ROLE" in
app) INSTANCE="${{ secrets.APP_INSTANCE }}" ;;
ocr) INSTANCE="${{ secrets.OCR_INSTANCE }}" ;;
alert) INSTANCE="${{ secrets.ALERT_INSTANCE }}" ;;
esac
echo "name=$INSTANCE" >> $GITHUB_OUTPUT
echo "🎯 Deploying to $INSTANCE"

- name: Sync deploy configs
if: steps.check.outputs.skip != 'true'
run: |
gcloud compute scp --recurse \
./compose ./config ./env ./scripts \
${{ steps.instance.outputs.name }}:~/depoly/ \
--zone=${{ secrets.GCE_ZONE }} \
--quiet

- name: Deploy via SSH
if: steps.check.outputs.skip != 'true'
run: |
gcloud compute ssh ${{ steps.instance.outputs.name }} \
--zone=${{ secrets.GCE_ZONE }} \
--quiet \
--command="
cd ~/depoly &&
gcloud auth configure-docker ${{ secrets.AR_REGION }}-docker.pkg.dev --quiet &&
source env/hosts.env &&
docker compose -f compose/${{ matrix.instance.compose }} pull &&
docker compose -f compose/${{ matrix.instance.compose }} up -d &&
echo '=== Container Status ===' &&
docker compose -f compose/${{ matrix.instance.compose }} ps
"

cd /home/ubuntu/app

docker compose \
-f docker-compose.backend.yml \
-f docker-compose.portainer.yml \
-f docker-compose.traefik.yml \
pull

docker compose \
-f docker-compose.backend.yml \
-f docker-compose.portainer.yml \
-f docker-compose.traefik.yml \
down

docker image prune -f
docker volume prune -f

docker compose \
-f docker-compose.backend.yml \
-f docker-compose.portainer.yml \
-f docker-compose.traefik.yml \
up -d --build
- name: Health check
if: steps.check.outputs.skip != 'true'
run: |
echo "⏳ Waiting 15s for services to start..."
sleep 15
gcloud compute ssh ${{ steps.instance.outputs.name }} \
--zone=${{ secrets.GCE_ZONE }} \
--quiet \
--command="docker compose -f ~/depoly/compose/${{ matrix.instance.compose }} ps --format 'table {{.Name}}\t{{.Status}}'"
103 changes: 64 additions & 39 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,54 +1,79 @@
name: CI - Upload Compose & Traefik Files
name: CI - Validate Deploy Configs

on:
push:
branches:
- main
paths-ignore:
- '.github/workflows/cd.yml'
branches: [main, develop]
pull_request:
branches: [main, develop]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
upload-files:
validate-yaml:
name: YAML Lint
runs-on: ubuntu-latest
environment: production

steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
lfs: true

- name: 디버깅 - source 파일 실제 존재 여부 확인
- uses: actions/checkout@v4
- name: Install yamllint
run: pip install yamllint
- name: Lint YAML files
run: |
echo "[INFO] 현재 경로: $(pwd)"
echo "[INFO] docker-compose.*.yml 파일 확인"
ls -l docker-compose.*.yml || echo "❌ 파일 없음"
echo "[INFO] traefik 디렉토리 내 파일"
ls -l traefik || echo "❌ traefik 폴더 없음"
yamllint -d relaxed compose/
yamllint -d relaxed config/

- name: Create tar archive
validate-compose:
name: Docker Compose Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set dummy environment variables
run: |
tar -czvf deploy_bundle.tar.gz \
docker-compose.backend.yml \
docker-compose.portainer.yml \
docker-compose.traefik.yml \
traefik

- name: Save SSH Key
# compose 파일에서 사용하는 환경변수에 더미 값 설정
cat env/hosts.env.example | sed 's/=.*/=dummy/' > .env
echo "ARTIFACT_REGISTRY=dummy.pkg.dev/project/repo" >> .env
- name: Validate compose files
run: |
echo "${{ secrets.SERVER_PEM_KEY }}" > private_key.pem
chmod 600 private_key.pem
for f in compose/docker-compose.*.yml; do
echo "Validating $f..."
docker compose -f "$f" config --quiet 2>&1 || echo "WARNING: $f has issues (may need env vars)"
done

- name: Upload to server
validate-scripts:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
run: |
scp -i private_key.pem -o StrictHostKeyChecking=no \
deploy_bundle.tar.gz ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:/home/ubuntu/app/
shellcheck scripts/*.sh || true

- name: Extract on server
validate-templates:
name: Template Variables Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check template variables are defined
run: |
ssh -i private_key.pem -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
cd /home/ubuntu/app
tar -xzvf deploy_bundle.tar.gz
rm deploy_bundle.tar.gz
EOF
# hosts.env.example에 정의된 변수 추출
defined_vars=$(grep -oP '^\w+' env/hosts.env.example | sort)

# 템플릿 파일에서 사용하는 변수 추출
template_vars=$(grep -orhP '\$\{(\w+)\}' config/ | grep -oP '\w+' | sort -u)

echo "=== hosts.env.example에 정의된 변수 ==="
echo "$defined_vars"
echo ""
echo "=== 템플릿에서 사용하는 변수 ==="
echo "$template_vars"
echo ""

# 누락된 변수 확인
missing=$(comm -23 <(echo "$template_vars") <(echo "$defined_vars"))
if [ -n "$missing" ]; then
echo "⚠️ 템플릿에서 사용하지만 hosts.env.example에 없는 변수:"
echo "$missing"
else
echo "✅ 모든 템플릿 변수가 hosts.env.example에 정의되어 있습니다."
fi
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
.idea
*.env
*.log
!*.env.example
*.log
config/traefik/dynamic_conf.yml
config/traefik/acme.json
config/traefik/certs/
config/monitoring/prometheus/prometheus.yml
config/monitoring/promtail/promtail-config.yml
config/monitoring/mysqld-exporter/.my.cnf
config/credentials/
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,57 @@
# depoly
# SpeedCam Deploy

6대 GCE 인스턴스의 프로덕션 compose, 인프라 설정, 배포 파이프라인을 조율한다.

## 디렉토리 구조

```
depoly/
├── compose/ # 인스턴스별 docker-compose 파일
├── config/ # Traefik, 모니터링, MySQL 설정
├── env/ # 환경변수 템플릿 (*.env.example)
├── scripts/ # 배포 스크립트
├── docs/ # 배포 가이드
└── .github/workflows/ # CI/CD 파이프라인
```

## 배포 대상 인스턴스

| 인스턴스명 | Zone | 역할 | 상태 |
|-----------|------|------|------|
| api-primary | us-central1-a | API 서버 (Primary) | Active |
| api-secondary | us-central1-b | API 서버 (Secondary) | Active |
| web-primary | us-east1-b | Web 서버 (Primary) | Active |
| web-secondary | us-east1-c | Web 서버 (Secondary) | Active |
| db-master | us-central1-a | MySQL Master | Active |
| db-replica | us-central1-c | MySQL Replica | Active |

## 빠른 시작

### 전체 배포

```bash
./scripts/deploy-all.sh <zone>
```

예시:
```bash
./scripts/deploy-all.sh us-central1-a
```

### 개별 배포

각 인스턴스 배포는 `docs/manual-deploy.md` 참조

## 저장소 책임 범위

이 저장소(depoly)는 다음을 관리한다:

- **Docker Compose 파일**: 각 GCE 인스턴스에서 실행되는 서비스 정의
- **인프라 설정**: Traefik (리버스 프록시), 모니터링 스택 (Prometheus, Grafana), MySQL 설정
- **환경 설정**: 환경변수 템플릿 및 설정 파일
- **배포 자동화**: 배포 스크립트 및 CI/CD 파이프라인
- **배포 문서**: 수동 배포 가이드 및 트러블슈팅

## 상세 배포 가이드

자세한 배포 절차는 [`docs/manual-deploy.md`](./docs/manual-deploy.md)를 참조하시기 바랍니다.
33 changes: 33 additions & 0 deletions compose/docker-compose.alert.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# speedcam-alert 인스턴스에 배포
services:
alert-worker:
image: ${ARTIFACT_REGISTRY}/speedcam-alert:latest
container_name: speedcam-alert
restart: always
network_mode: host
env_file:
- ../env/backend.env
volumes:
- ../config/credentials:/app/credentials:ro

promtail:
image: grafana/promtail:2.9.6
container_name: speedcam-promtail
restart: always
network_mode: host
volumes:
- ../config/monitoring/promtail/promtail-config.yml:/etc/promtail/config.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml

cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
container_name: speedcam-cadvisor
restart: always
network_mode: host
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
Loading