diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d15a87d..62ca227 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -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 \ No newline at end of file + - 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}}'" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b174c90..c77ef11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index a01d507..6dd7ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ .idea *.env -*.log \ No newline at end of file +!*.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/ diff --git a/README.md b/README.md index c7dae7c..773964b 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ -# depoly \ No newline at end of file +# 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 +``` + +예시: +```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)를 참조하시기 바랍니다. \ No newline at end of file diff --git a/compose/docker-compose.alert.yml b/compose/docker-compose.alert.yml new file mode 100644 index 0000000..2e93f17 --- /dev/null +++ b/compose/docker-compose.alert.yml @@ -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 diff --git a/compose/docker-compose.app.yml b/compose/docker-compose.app.yml new file mode 100644 index 0000000..5505119 --- /dev/null +++ b/compose/docker-compose.app.yml @@ -0,0 +1,58 @@ +# speedcam-app 인스턴스에 배포 +# Traefik: 리버스 프록시 (IP 모드 / 도메인 모드 겸용) +# Main: Django API 서버 +# Flower: Celery 모니터링 +services: + traefik: + image: traefik:v3.0 + container_name: speedcam-traefik + restart: always + network_mode: host + volumes: + - ../config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro + - ../config/traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro + - traefik_certs:/etc/traefik/certs + + main: + image: ${ARTIFACT_REGISTRY}/speedcam-main:latest + container_name: speedcam-main + restart: always + network_mode: host + env_file: + - ../env/backend.env + volumes: + - ../config/credentials:/app/credentials:ro + + flower: + image: ${ARTIFACT_REGISTRY}/speedcam-main:latest + container_name: speedcam-flower + restart: always + network_mode: host + env_file: + - ../env/backend.env + command: celery -A config flower --port=5555 + + 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 + +volumes: + traefik_certs: diff --git a/compose/docker-compose.db.yml b/compose/docker-compose.db.yml new file mode 100644 index 0000000..7096035 --- /dev/null +++ b/compose/docker-compose.db.yml @@ -0,0 +1,55 @@ +# speedcam-db 인스턴스에 배포 +services: + mysql: + image: mysql:8.0 + container_name: speedcam-mysql + restart: always + network_mode: host + env_file: + - ../env/mysql.env + volumes: + - mysql_data:/var/lib/mysql + - ../config/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + mysqld-exporter: + image: prom/mysqld-exporter:v0.15.1 + container_name: speedcam-mysqld-exporter + restart: always + network_mode: host + volumes: + - ../config/monitoring/mysqld-exporter/.my.cnf:/cfg/.my.cnf:ro + command: + - "--config.my-cnf=/cfg/.my.cnf" + depends_on: + mysql: + condition: service_healthy + + 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 + +volumes: + mysql_data: diff --git a/compose/docker-compose.mon.yml b/compose/docker-compose.mon.yml new file mode 100644 index 0000000..d6d7a33 --- /dev/null +++ b/compose/docker-compose.mon.yml @@ -0,0 +1,91 @@ +# speedcam-mon 인스턴스에 배포 +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:0.98.0 + container_name: speedcam-otel-collector + restart: always + network_mode: host + volumes: + - ../config/monitoring/otel-collector/otel-collector-config.yml:/etc/otel-collector-config.yml:ro + command: ["--config", "/etc/otel-collector-config.yml"] + + jaeger: + image: jaegertracing/all-in-one:1.57 + container_name: speedcam-jaeger + restart: always + network_mode: host + environment: + - COLLECTOR_OTLP_ENABLED=true + - COLLECTOR_OTLP_GRPC_HOST_PORT=:4320 + - COLLECTOR_OTLP_HTTP_HOST_PORT=:4321 + + prometheus: + image: prom/prometheus:v2.51.2 + container_name: speedcam-prometheus + restart: always + network_mode: host + volumes: + - ../config/monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.retention.time=15d" + - "--web.enable-remote-write-receiver" + + grafana: + image: grafana/grafana:10.4.2 + container_name: speedcam-grafana + restart: always + network_mode: host + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin} + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - ../config/monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - grafana_data:/var/lib/grafana + + loki: + image: grafana/loki:2.9.6 + container_name: speedcam-loki + restart: always + network_mode: host + volumes: + - ../config/monitoring/loki/loki-config.yml:/etc/loki/local-config.yaml:ro + - loki_data:/loki + command: -config.file=/etc/loki/local-config.yaml + + promtail: + image: grafana/promtail:2.9.6 + container_name: speedcam-promtail + restart: always + network_mode: host + volumes: + - ../config/monitoring/promtail/promtail-config.mon.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 + + celery-exporter: + image: danihodovic/celery-exporter:0.10.3 + container_name: speedcam-celery-exporter + restart: always + network_mode: host + environment: + CE_BROKER_URL: "amqp://sa:${RABBITMQ_PASSWORD}@${MQ_HOST}:5672//" + + 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 + +volumes: + prometheus_data: + grafana_data: + loki_data: diff --git a/compose/docker-compose.mq.yml b/compose/docker-compose.mq.yml new file mode 100644 index 0000000..22c6ee9 --- /dev/null +++ b/compose/docker-compose.mq.yml @@ -0,0 +1,44 @@ +# speedcam-mq 인스턴스에 배포 +services: + rabbitmq: + image: rabbitmq:3.13-management + container_name: speedcam-rabbitmq + restart: always + network_mode: host + env_file: + - ../env/rabbitmq.env + volumes: + - rabbitmq_data:/var/lib/rabbitmq + command: > + bash -c "rabbitmq-plugins enable --offline rabbitmq_mqtt rabbitmq_prometheus && + rabbitmq-server" + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "check_running"] + interval: 10s + timeout: 5s + retries: 5 + + 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 + +volumes: + rabbitmq_data: diff --git a/compose/docker-compose.ocr.yml b/compose/docker-compose.ocr.yml new file mode 100644 index 0000000..95cf379 --- /dev/null +++ b/compose/docker-compose.ocr.yml @@ -0,0 +1,33 @@ +# speedcam-ocr 인스턴스에 배포 +services: + ocr-worker: + image: ${ARTIFACT_REGISTRY}/speedcam-ocr:latest + container_name: speedcam-ocr + 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 diff --git a/config/monitoring/grafana/provisioning/datasources/datasources.yml b/config/monitoring/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..ada6061 --- /dev/null +++ b/config/monitoring/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,32 @@ +# =========================================== +# Grafana 데이터소스 (모두 localhost) +# =========================================== + +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://localhost:9090 + isDefault: true + editable: false + + - name: Jaeger + type: jaeger + access: proxy + url: http://localhost:16686 + editable: false + + - name: Loki + type: loki + access: proxy + url: http://localhost:3100 + editable: false + jsonData: + derivedFields: + - datasourceUid: jaeger + matcherRegex: "trace_id=(\\w+)" + name: TraceID + url: "$${__value.raw}" + datasourceName: Jaeger diff --git a/config/monitoring/loki/loki-config.yml b/config/monitoring/loki/loki-config.yml new file mode 100644 index 0000000..1faaf43 --- /dev/null +++ b/config/monitoring/loki/loki-config.yml @@ -0,0 +1,32 @@ +# =========================================== +# Loki 설정 +# =========================================== + +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +limits_config: + retention_period: 168h diff --git a/config/monitoring/mysqld-exporter/.my.cnf.example b/config/monitoring/mysqld-exporter/.my.cnf.example new file mode 100644 index 0000000..20cbd6a --- /dev/null +++ b/config/monitoring/mysqld-exporter/.my.cnf.example @@ -0,0 +1,10 @@ +# =========================================== +# MySQL Exporter 설정 +# =========================================== +# cp .my.cnf.example .my.cnf 후 비밀번호 수정 + +[client] +user=sa +password= +host=localhost +port=3306 diff --git a/config/monitoring/otel-collector/otel-collector-config.yml b/config/monitoring/otel-collector/otel-collector-config.yml new file mode 100644 index 0000000..ac15193 --- /dev/null +++ b/config/monitoring/otel-collector/otel-collector-config.yml @@ -0,0 +1,43 @@ +# =========================================== +# OpenTelemetry Collector 설정 +# =========================================== + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 5s + send_batch_size: 1024 + resource: + attributes: + - key: service.namespace + value: speedcam + action: upsert + +exporters: + otlp/jaeger: + endpoint: localhost:4320 + tls: + insecure: true + prometheus: + endpoint: 0.0.0.0:8889 + namespace: speedcam + resource_to_telemetry_conversion: + enabled: true + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource] + exporters: [otlp/jaeger] + metrics: + receivers: [otlp] + processors: [batch, resource] + exporters: [prometheus] diff --git a/config/monitoring/prometheus/prometheus.yml.template b/config/monitoring/prometheus/prometheus.yml.template new file mode 100644 index 0000000..8196d83 --- /dev/null +++ b/config/monitoring/prometheus/prometheus.yml.template @@ -0,0 +1,42 @@ +# =========================================== +# Prometheus 설정 (envsubst 템플릿) +# =========================================== + +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "django" + metrics_path: /metrics + static_configs: + - targets: ["${APP_HOST}:8000"] + + - job_name: "otel-collector" + static_configs: + - targets: ["localhost:8889"] + + - job_name: "rabbitmq" + static_configs: + - targets: ["${MQ_HOST}:15692"] + + - job_name: "mysql" + static_configs: + - targets: ["${DB_HOST}:9104"] + + - job_name: "celery" + static_configs: + - targets: ["localhost:9808"] + + - job_name: "cadvisor" + static_configs: + - targets: + - "${DB_HOST}:8080" + - "${MQ_HOST}:8080" + - "${APP_HOST}:8080" + - "${OCR_HOST}:8080" + - "${ALERT_HOST}:8080" + - "localhost:8080" + relabel_configs: + - source_labels: [__address__] + target_label: instance diff --git a/config/monitoring/promtail/promtail-config.mon.yml b/config/monitoring/promtail/promtail-config.mon.yml new file mode 100644 index 0000000..b773e52 --- /dev/null +++ b/config/monitoring/promtail/promtail-config.mon.yml @@ -0,0 +1,36 @@ +# =========================================== +# Promtail 설정 (모니터링 인스턴스 전용) +# =========================================== +# Loki가 같은 인스턴스이므로 localhost + +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://localhost:3100/loki/api/v1/push + +scrape_configs: + - job_name: docker + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + filters: + - name: name + values: + - "speedcam-.*" + relabel_configs: + - source_labels: ["__meta_docker_container_name"] + regex: "/(.*)" + target_label: "container" + - source_labels: ["__meta_docker_container_name"] + regex: "/speedcam-(.*)" + target_label: "service" + pipeline_stages: + - regex: + expression: ".*trace_id=(?P[a-f0-9]+).*" + - labels: + trace_id: diff --git a/config/monitoring/promtail/promtail-config.yml.template b/config/monitoring/promtail/promtail-config.yml.template new file mode 100644 index 0000000..ae83b4c --- /dev/null +++ b/config/monitoring/promtail/promtail-config.yml.template @@ -0,0 +1,36 @@ +# =========================================== +# Promtail 설정 (envsubst 템플릿) +# =========================================== +# 앱/워커/DB/MQ 인스턴스 공통 + +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://${MON_HOST}:3100/loki/api/v1/push + +scrape_configs: + - job_name: docker + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + filters: + - name: name + values: + - "speedcam-.*" + relabel_configs: + - source_labels: ["__meta_docker_container_name"] + regex: "/(.*)" + target_label: "container" + - source_labels: ["__meta_docker_container_name"] + regex: "/speedcam-(.*)" + target_label: "service" + pipeline_stages: + - regex: + expression: ".*trace_id=(?P[a-f0-9]+).*" + - labels: + trace_id: diff --git a/config/mysql/init.sql b/config/mysql/init.sql new file mode 100644 index 0000000..683680d --- /dev/null +++ b/config/mysql/init.sql @@ -0,0 +1,15 @@ +-- =========================================== +-- SpeedCam MSA 데이터베이스 초기화 +-- =========================================== +-- MySQL 컨테이너 최초 실행 시 자동 실행 + +CREATE DATABASE IF NOT EXISTS speedcam_vehicles CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE IF NOT EXISTS speedcam_detections CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE IF NOT EXISTS speedcam_notifications CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER DATABASE speedcam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +GRANT ALL PRIVILEGES ON speedcam_vehicles.* TO 'sa'@'%'; +GRANT ALL PRIVILEGES ON speedcam_detections.* TO 'sa'@'%'; +GRANT ALL PRIVILEGES ON speedcam_notifications.* TO 'sa'@'%'; +FLUSH PRIVILEGES; diff --git a/config/traefik/dynamic_conf.domain.yml.template b/config/traefik/dynamic_conf.domain.yml.template new file mode 100644 index 0000000..4ea3e3b --- /dev/null +++ b/config/traefik/dynamic_conf.domain.yml.template @@ -0,0 +1,131 @@ +# =========================================== +# Traefik 동적 설정 -- 도메인 모드 (envsubst 템플릿) +# =========================================== +# 생성: scripts/setup-env.sh 실행 시 자동 생성 +# 접근: +# - https://api.${DOMAIN} -> Django API +# - https://flower.${DOMAIN} -> Flower +# - https://grafana.${DOMAIN} -> Grafana +# - https://rabbitmq.${DOMAIN} -> RabbitMQ UI +# - https://traefik.${DOMAIN} -> Traefik Dashboard + +http: + routers: + # HTTP -> HTTPS 리다이렉트 + http-redirect: + rule: "PathPrefix(`/`)" + entryPoints: + - web + middlewares: + - redirect-to-https + service: noop@internal + priority: 1 + + # Django API + api: + rule: "Host(`api.${DOMAIN}`)" + entryPoints: + - websecure + service: api + tls: + certResolver: letsencrypt + middlewares: + - cors + - rate-limit + + # Flower (Celery 모니터링) + flower: + rule: "Host(`flower.${DOMAIN}`)" + entryPoints: + - websecure + service: flower + tls: + certResolver: letsencrypt + middlewares: + - admin-auth + + # Grafana + grafana: + rule: "Host(`grafana.${DOMAIN}`)" + entryPoints: + - websecure + service: grafana + tls: + certResolver: letsencrypt + + # RabbitMQ Management UI + rabbitmq: + rule: "Host(`rabbitmq.${DOMAIN}`)" + entryPoints: + - websecure + service: rabbitmq + tls: + certResolver: letsencrypt + middlewares: + - admin-auth + + # Traefik Dashboard + traefik-dashboard: + rule: "Host(`traefik.${DOMAIN}`)" + entryPoints: + - websecure + service: api@internal + tls: + certResolver: letsencrypt + middlewares: + - admin-auth + + services: + api: + loadBalancer: + servers: + - url: "http://localhost:8000" + + flower: + loadBalancer: + servers: + - url: "http://localhost:5555" + + grafana: + loadBalancer: + servers: + - url: "http://${MON_HOST}:3000" + + rabbitmq: + loadBalancer: + servers: + - url: "http://${MQ_HOST}:15672" + + middlewares: + redirect-to-https: + redirectScheme: + scheme: https + permanent: true + + cors: + headers: + accessControlAllowOriginList: + - "https://${DOMAIN}" + - "https://www.${DOMAIN}" + - "https://api.${DOMAIN}" + accessControlAllowMethods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + accessControlAllowHeaders: + - "*" + accessControlAllowCredentials: true + addVaryHeader: true + + rate-limit: + rateLimit: + average: 50 + burst: 100 + + admin-auth: + basicAuth: + users: + - "${TRAEFIK_AUTH_USER}" diff --git a/config/traefik/dynamic_conf.ip.yml b/config/traefik/dynamic_conf.ip.yml new file mode 100644 index 0000000..9f02ece --- /dev/null +++ b/config/traefik/dynamic_conf.ip.yml @@ -0,0 +1,48 @@ +# =========================================== +# Traefik 동적 설정 -- IP 모드 (도메인 없음) +# =========================================== +# 외부 트래픽: http:// -> Django API +# 관리 도구: VPC 내부 IP로 직접 접근 +# - Flower: http://:5555 +# - Grafana: http://:3000 +# - RabbitMQ: http://:15672 + +http: + routers: + # 모든 HTTP 트래픽 -> Django API + api: + rule: "PathPrefix(`/`)" + entryPoints: + - web + service: api + middlewares: + - cors + - rate-limit + + services: + api: + loadBalancer: + servers: + - url: "http://localhost:8000" + + middlewares: + cors: + headers: + accessControlAllowOriginList: + - "*" + accessControlAllowMethods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + accessControlAllowHeaders: + - "*" + accessControlAllowCredentials: true + addVaryHeader: true + + rate-limit: + rateLimit: + average: 50 + burst: 100 diff --git a/config/traefik/traefik.yml b/config/traefik/traefik.yml new file mode 100644 index 0000000..f08d11b --- /dev/null +++ b/config/traefik/traefik.yml @@ -0,0 +1,34 @@ +# =========================================== +# Traefik 정적 설정 +# =========================================== +# IP 모드 / 도메인 모드 공용 + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + +api: + dashboard: true + insecure: false + +log: + level: INFO + +accessLog: {} + +# file provider만 사용 (host 모드에서는 docker provider 불가) +providers: + file: + filename: "/etc/traefik/dynamic_conf.yml" + watch: true + +# SSL 인증서 (도메인 모드에서만 사용됨, IP 모드에서는 무시) +certificatesResolvers: + letsencrypt: + acme: + email: placeholder@example.com + storage: /etc/traefik/certs/acme.json + httpChallenge: + entryPoint: web diff --git a/docker-compose.backend.yml b/docker-compose.backend.yml deleted file mode 100644 index 41fcdd6..0000000 --- a/docker-compose.backend.yml +++ /dev/null @@ -1,142 +0,0 @@ -version: "3.9" - -services: - mysqldb: - image: mysql:latest - container_name: mysqldb - environment: - MYSQL_USER: ${MYSQL_USER} - MYSQL_PASSWORD: ${MYSQL_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - ports: - - "3306:3306" - networks: - - app-network - - backend: - image: ${DOCKER_USERNAME}/${DOCKER_IMAGE_NAME}:latest - container_name: backend - volumes: - - /home/ubuntu/app/secrets/firebase-credentials.json:/app/secrets/firebase-credentials.json:ro - environment: - - DJANGO_SETTINGS_MODULE=backend.settings.prod - - MYSQL_DATABASE=${MYSQL_DATABASE} - - MYSQL_USER=${MYSQL_USER} - - MYSQL_PASSWORD=${MYSQL_PASSWORD} - - SECRET_KEY=${SECRET_KEY} - - GOOGLE_APPLICATION_CREDENTIALS=/app/secrets/firebase-credentials.json - - AWS_ACCESS_KEY=${AWS_ACCESS_KEY} - - AWS_SECRET_KEY=${AWS_SECRET_KEY} - - AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME} - - AWS_S3_REGION=${AWS_S3_REGION} - - command: > - bash -c "python wait_mysql.py && - python manage.py migrate && - exec gunicorn backend.wsgi:application --bind 0.0.0.0:8000 --workers=4 --threads=2" - depends_on: - - mysqldb - networks: - - app-network - - web - - rabbitmq: - image: "rabbitmq:3.13-management" - container_name: rabbitmq - environment: - - CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672// - - RABBITMQ_USER=${RABBITMQ_USER} - - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - ports: - # Expose the port for the worker to add/get tasks - - 5672:5672 - # OPTIONAL: Expose the GUI port - - 15672:15672 - depends_on: - - backend - restart: always - tty: true # restart: unless-stopped?? - expose: - - 5672 - networks: - - app-network - - web - - celery_worker: - image: ${DOCKER_USERNAME}/${DOCKER_IMAGE_NAME}:latest - container_name: celery_worker - command: celery -A backend worker --concurrency=4 --loglevel=info - volumes: - - /home/ubuntu/app/secrets/firebase-credentials.json:/app/secrets/firebase-credentials.json:ro - environment: - - CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672// - - DJANGO_SETTINGS_MODULE=backend.settings.prod - - MYSQL_DATABASE=${MYSQL_DATABASE} - - MYSQL_USER=${MYSQL_USER} - - MYSQL_PASSWORD=${MYSQL_PASSWORD} - - GOOGLE_APPLICATION_CREDENTIALS=/app/secrets/firebase-credentials.json - - depends_on: - - rabbitmq - - backend - restart: always - tty: true - networks: - - app-network - - celery_worker_dlq: - image: ${DOCKER_USERNAME}/${DOCKER_IMAGE_NAME}:latest - container_name: celery_worker_dlq - command: celery -A backend worker --loglevel=info -Q dlq_notify_queue - volumes: - - /home/ubuntu/app/secrets/firebase-credentials.json:/app/secrets/firebase-credentials.json:ro - environment: - - CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672// - - DJANGO_SETTINGS_MODULE=backend.settings.prod - - MYSQL_DATABASE=${MYSQL_DATABASE} - - MYSQL_USER=${MYSQL_USER} - - MYSQL_PASSWORD=${MYSQL_PASSWORD} - - GOOGLE_APPLICATION_CREDENTIALS=/app/secrets/firebase-credentials.json - depends_on: - - rabbitmq - - backend - restart: always - tty: true - networks: - - app-network - - celery_beat: - image: ${DOCKER_USERNAME}/${DOCKER_IMAGE_NAME}:latest - container_name: celery_beat - command: sh -c "celery -A backend beat --loglevel=info" - environment: - - CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672// - - DJANGO_SETTINGS_MODULE=backend.settings.prod - depends_on: - - rabbitmq - - backend - restart: always - tty: true - networks: - - app-network - - flower: - image: mher/flower - container_name: flower - environment: - - CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672/ - - TZ=Asia/Seoul - ports: - - '5555:5555' - depends_on: - - rabbitmq - - celery_worker - - celery_beat - networks: - - app-network - - web - -networks: - app-network: - external: true diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml deleted file mode 100644 index 3f6cc67..0000000 --- a/docker-compose.portainer.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: "3.9" - -services: - portainer: - image: portainer/portainer-ce - container_name: portainer -# ports: -# - "9443:9443" -# - "9000:9000" - expose: - - "9000" - - "9443" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - portainer_data:/data - networks: - - web - - app-network - labels: - - "traefik.enable=true" - - "traefik.http.routers.portainer.rule=Host(`portainer.autonotify.store`)" - - "traefik.http.routers.portainer.entrypoints=websecure" - - "traefik.http.routers.portainer.tls.certresolver=letsencrypt" - - "traefik.http.services.portainer.loadbalancer.server.port=9000" - - "traefik.http.routers.portainer.middlewares=auth@file" - - agent: - image: portainer/agent:latest - container_name: portainer_agent - environment: - - AGENT_CLUSTER_ADDR=tasks.agent - - DOCKER_HOST=unix:///var/run/docker.sock - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /var/lib/docker/volumes:/var/lib/docker/volumes - networks: - - web - - app-network - restart: always - -volumes: - portainer_data: - -networks: - web: - external: true - - app-network: - external: true diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml deleted file mode 100644 index 21bdee9..0000000 --- a/docker-compose.traefik.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: "3.9" - -services: - traefik: - image: traefik:v3.0 - container_name: traefik - restart: always - ports: - - "80:80" - - "443:443" - - "8088:8088" - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro - - ./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro - - ./traefik/letsencrypt:/etc/traefik # letsencrypt 데이터용 디렉토리 마운트 - networks: - - web # web 네트워크 연결 - environment: - - TRAEFIK_DASHBOARD_AUTH=autonotify:$2y$05$nk3Iz..3eY6AhbgWcPJz/.hv2Wvbnq6ddgSjrnoLsxDvMPWdm4Jga # 대시보드 인증 정보 - labels: - # 보안 대시보드 라우터 설정 (traefik.yourdomain.com 교체 필요) - - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.autonotify.store`)" - - "traefik.http.routers.traefik-dashboard.entrypoints=websecure" - - "traefik.http.routers.traefik-dashboard.service=api@internal" - - "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt" - - "traefik.http.routers.traefik-dashboard.middlewares=dashboard-auth@file" - -networks: - app-network: - external: true - - web: - external: true diff --git a/docs/manual-deploy.md b/docs/manual-deploy.md new file mode 100644 index 0000000..83a926b --- /dev/null +++ b/docs/manual-deploy.md @@ -0,0 +1,1199 @@ +# SpeedCam GCE 수동 배포 가이드 + +## 목차 +1. [사전 요구사항](#1-사전-요구사항) +2. [Artifact Registry 설정](#2-artifact-registry-설정) +3. [Docker 이미지 빌드 및 푸시](#3-docker-이미지-빌드-및-푸시) +4. [환경 설정 파일 준비](#4-환경-설정-파일-준비) +5. [Credentials 디렉토리 생성](#5-credentials-디렉토리-생성) +6. [MySQL Exporter 설정](#6-mysql-exporter-설정) +7. [인스턴스별 배포 순서](#7-인스턴스별-배포-순서) +8. [헬스체크 및 검증](#8-헬스체크-및-검증) +9. [롤백 절차](#9-롤백-절차) +10. [이미지 버전 동기화 체크리스트](#10-이미지-버전-동기화-체크리스트) +11. [트러블슈팅](#11-트러블슈팅) + +--- + +## 1. 사전 요구사항 + +### 로컬 환경 +- **gcloud CLI**: 설치 및 인증 완료 +- **Docker**: 로컬에 설치되어 있어야 함 +- **Git**: backend 레포지토리 클론 필요 + +```bash +# gcloud 설치 확인 +gcloud version + +# gcloud 인증 +gcloud auth login +gcloud config set project + +# Docker 설치 확인 +docker --version +``` + +### GCP 프로젝트 +- GCE 인스턴스 6개 생성 완료 + - `speedcam-db` (MySQL) + - `speedcam-mq` (RabbitMQ) + - `speedcam-app` (Traefik + Django API) + - `speedcam-ocr` (OCR Worker) + - `speedcam-alert` (Alert Worker) + - `speedcam-mon` (Monitoring Stack) +- VPC 네트워크 설정 완료 (내부 IP 통신 가능) +- 방화벽 규칙 설정 완료 + +### 인스턴스 정보 확인 + +```bash +# 인스턴스 내부 IP 확인 +gcloud compute instances list --filter="name~speedcam" \ + --format="table(name, networkInterfaces[0].networkIP, networkInterfaces[0].accessConfigs[0].natIP)" +``` + +**예시 출력:** +``` +NAME INTERNAL_IP EXTERNAL_IP +speedcam-db 10.178.0.11 34.xxx.xxx.xxx +speedcam-mq 10.178.0.12 34.xxx.xxx.xxx +speedcam-app 10.178.0.13 34.xxx.xxx.xxx +speedcam-ocr 10.178.0.14 34.xxx.xxx.xxx +speedcam-alert 10.178.0.15 34.xxx.xxx.xxx +speedcam-mon 10.178.0.20 34.xxx.xxx.xxx +``` + +--- + +## 2. Artifact Registry 설정 + +### 저장소 생성 + +```bash +# Artifact Registry 저장소 생성 (asia-northeast3 리전) +gcloud artifacts repositories create speedcam \ + --repository-format=docker \ + --location=asia-northeast3 \ + --description="SpeedCam Docker Images" + +# 생성 확인 +gcloud artifacts repositories list --location=asia-northeast3 +``` + +### Docker 인증 설정 + +```bash +# Artifact Registry 인증 구성 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev +``` + +--- + +## 3. Docker 이미지 빌드 및 푸시 + +> **중요**: 이미지 빌드는 **backend 레포지토리**에서 수행합니다. + +### 3.1 Backend 레포지토리로 이동 + +```bash +# backend 레포지토리 클론 (아직 없다면) +git clone +cd + +# 최신 코드 풀 +git checkout main +git pull origin main +``` + +### 3.2 환경 변수 설정 + +```bash +# Artifact Registry 경로 설정 +export PROJECT_ID= +export REGION=asia-northeast3 +export ARTIFACT_REGISTRY="${REGION}-docker.pkg.dev/${PROJECT_ID}/speedcam" + +# 이미지 태그 설정 (버전 관리) +export IMAGE_TAG=$(date +%Y%m%d-%H%M%S) # 예: 20260208-143052 +# 또는 Git 커밋 해시 사용 +# export IMAGE_TAG=$(git rev-parse --short HEAD) +``` + +### 3.3 이미지 빌드 + +```bash +# 1. speedcam-main 이미지 (Django API 서버) +docker build -f docker/main.Dockerfile \ + -t ${ARTIFACT_REGISTRY}/speedcam-main:${IMAGE_TAG} \ + -t ${ARTIFACT_REGISTRY}/speedcam-main:latest \ + . + +# 2. speedcam-ocr 이미지 (OCR Worker) +docker build -f docker/ocr.Dockerfile \ + -t ${ARTIFACT_REGISTRY}/speedcam-ocr:${IMAGE_TAG} \ + -t ${ARTIFACT_REGISTRY}/speedcam-ocr:latest \ + . + +# 3. speedcam-alert 이미지 (Alert Worker) +docker build -f docker/alert.Dockerfile \ + -t ${ARTIFACT_REGISTRY}/speedcam-alert:${IMAGE_TAG} \ + -t ${ARTIFACT_REGISTRY}/speedcam-alert:latest \ + . +``` + +### 3.4 이미지 푸시 + +```bash +# speedcam-main 푸시 +docker push ${ARTIFACT_REGISTRY}/speedcam-main:${IMAGE_TAG} +docker push ${ARTIFACT_REGISTRY}/speedcam-main:latest + +# speedcam-ocr 푸시 +docker push ${ARTIFACT_REGISTRY}/speedcam-ocr:${IMAGE_TAG} +docker push ${ARTIFACT_REGISTRY}/speedcam-ocr:latest + +# speedcam-alert 푸시 +docker push ${ARTIFACT_REGISTRY}/speedcam-alert:${IMAGE_TAG} +docker push ${ARTIFACT_REGISTRY}/speedcam-alert:latest +``` + +### 3.5 푸시 확인 + +```bash +# 업로드된 이미지 확인 +gcloud artifacts docker images list ${ARTIFACT_REGISTRY} --include-tags +``` + +--- + +## 4. 환경 설정 파일 준비 + +### 4.1 depoly 레포지토리로 이동 + +```bash +cd /path/to/depoly +``` + +### 4.2 env 파일 복사 + +```bash +# 환경 변수 템플릿 복사 +cp env/hosts.env.example env/hosts.env +cp env/backend.env.example env/backend.env +cp env/mysql.env.example env/mysql.env +cp env/rabbitmq.env.example env/rabbitmq.env +``` + +### 4.3 hosts.env 수정 + +```bash +nano env/hosts.env +``` + +**수정 내용:** +```bash +# 인스턴스 내부 IP 설정 (실제 IP로 교체) +export DB_HOST=10.178.0.11 +export MQ_HOST=10.178.0.12 +export APP_HOST=10.178.0.13 +export OCR_HOST=10.178.0.14 +export ALERT_HOST=10.178.0.15 +export MON_HOST=10.178.0.20 + +# Artifact Registry 경로 (your-project-id 교체) +export ARTIFACT_REGISTRY=asia-northeast3-docker.pkg.dev//speedcam + +# RabbitMQ 비밀번호 +export RABBITMQ_PASSWORD= + +# Grafana 비밀번호 +export GRAFANA_PASSWORD= + +# 도메인 설정 (선택사항 - 없으면 비워두기) +export DOMAIN= +# 도메인이 있다면: export DOMAIN=autonotify.store +export ACME_EMAIL=your-email@example.com +export TRAEFIK_AUTH_USER=admin:$$2y$$05$$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 4.4 backend.env 수정 + +```bash +nano env/backend.env +``` + +**수정 내용:** +```bash +# Django +SECRET_KEY= +DJANGO_SETTINGS_MODULE=config.settings.prod +DEBUG=False + +# Database (hosts.env의 DB_HOST IP로 교체) +DB_HOST=10.178.0.11 +DB_PORT=3306 +DB_USER=sa +DB_PASSWORD= +DB_NAME=speedcam +DB_NAME_VEHICLES=speedcam_vehicles +DB_NAME_DETECTIONS=speedcam_detections +DB_NAME_NOTIFICATIONS=speedcam_notifications + +# RabbitMQ / Celery (hosts.env의 MQ_HOST IP로 교체) +CELERY_BROKER_URL=amqp://sa:@10.178.0.12:5672// +RABBITMQ_HOST=10.178.0.12 +MQTT_PORT=1883 +MQTT_USER=sa +MQTT_PASS= + +# GCS / Firebase +GOOGLE_APPLICATION_CREDENTIALS=/app/credentials/gcp-cloud-storage.json +FIREBASE_CREDENTIALS=/app/credentials/firebase-service-account.json + +# Workers +OCR_CONCURRENCY=4 +ALERT_CONCURRENCY=100 +OCR_MOCK=false +FCM_MOCK=false + +# Gunicorn +GUNICORN_WORKERS=4 +GUNICORN_THREADS=2 + +# Logging +LOG_LEVEL=info + +# CORS (프론트엔드 도메인 또는 IP) +CORS_ALLOWED_ORIGINS=http://localhost:3000 + +# OpenTelemetry (hosts.env의 MON_HOST IP로 교체) +OTEL_EXPORTER_OTLP_ENDPOINT=http://10.178.0.20:4317 +OTEL_EXPORTER_OTLP_PROTOCOL=grpc +OTEL_RESOURCE_ATTRIBUTES=service.namespace=speedcam,deployment.environment=prod +OTEL_TRACES_SAMPLER=parentbased_tracealways +OTEL_PYTHON_LOG_CORRELATION=true +``` + +### 4.5 mysql.env 수정 + +```bash +nano env/mysql.env +``` + +**수정 내용:** +```bash +MYSQL_ROOT_PASSWORD= +MYSQL_USER=sa +MYSQL_PASSWORD= +MYSQL_DATABASE=speedcam +``` + +### 4.6 rabbitmq.env 수정 + +```bash +nano env/rabbitmq.env +``` + +**수정 내용:** +```bash +RABBITMQ_DEFAULT_USER=sa +RABBITMQ_DEFAULT_PASS= +RABBITMQ_DEFAULT_VHOST=/ +``` + +### 4.7 setup-env.sh 실행 + +```bash +# hosts.env 로드 및 설정 파일 생성 +source env/hosts.env +./scripts/setup-env.sh +``` + +**생성되는 파일:** +- `config/traefik/dynamic_conf.yml` (도메인 모드 또는 IP 모드) +- `config/monitoring/prometheus/prometheus.yml` +- `config/monitoring/promtail/promtail-config.yml` + +--- + +## 5. Credentials 디렉토리 생성 + +### 5.1 GCS 서비스 계정 키 생성 (GCP Console에서) + +1. **IAM 및 관리자 → 서비스 계정**으로 이동 +2. 서비스 계정 생성: `speedcam-gcs-sa` +3. 역할 부여: **Storage 객체 관리자** +4. 키 생성 → JSON 다운로드 → `gcp-cloud-storage.json`으로 저장 + +### 5.2 Firebase 서비스 계정 키 생성 (Firebase Console에서) + +1. **프로젝트 설정 → 서비스 계정**으로 이동 +2. **새 비공개 키 생성** 클릭 +3. JSON 다운로드 → `firebase-service-account.json`으로 저장 + +### 5.3 credentials 디렉토리 생성 + +```bash +# depoly 레포지토리에 credentials 디렉토리 생성 +mkdir -p config/credentials + +# 다운로드한 JSON 파일을 credentials 디렉토리로 이동 +mv ~/Downloads/gcp-cloud-storage.json config/credentials/ +mv ~/Downloads/firebase-service-account.json config/credentials/ + +# 권한 설정 +chmod 600 config/credentials/*.json +``` + +**디렉토리 구조:** +``` +config/ +└── credentials/ + ├── gcp-cloud-storage.json + └── firebase-service-account.json +``` + +--- + +## 6. MySQL Exporter 설정 + +### 6.1 MySQL Exporter 설정 파일 생성 + +```bash +# 디렉토리 생성 +mkdir -p config/monitoring/mysqld-exporter + +# 설정 파일 생성 +nano config/monitoring/mysqld-exporter/.my.cnf +``` + +**파일 내용:** +```ini +[client] +user=sa +password= +host=localhost +port=3306 +``` + +### 6.2 권한 설정 + +```bash +chmod 600 config/monitoring/mysqld-exporter/.my.cnf +``` + +--- + +## 7. 인스턴스별 배포 순서 + +> **배포 순서**: DB → MQ → MON → APP → OCR → ALERT + +### 공통 준비 작업 + +```bash +# 배포 디렉토리 압축 (로컬에서) +cd /path/to/depoly +tar -czf speedcam-deploy.tar.gz \ + compose/ \ + config/ \ + env/ \ + scripts/ + +# 압축 파일 확인 +ls -lh speedcam-deploy.tar.gz +``` + +--- + +### 7.1 speedcam-db 배포 + +#### 1) SCP로 파일 전송 + +```bash +# 배포 파일 전송 +gcloud compute scp speedcam-deploy.tar.gz speedcam-db:~/ --zone=asia-northeast3-a + +# 또는 외부 IP 직접 사용 +# scp speedcam-deploy.tar.gz username@34.xxx.xxx.xxx:~/ +``` + +#### 2) SSH 접속 및 배포 + +```bash +# SSH 접속 +gcloud compute ssh speedcam-db --zone=asia-northeast3-a +``` + +**인스턴스 내부 작업:** + +```bash +# 압축 해제 +tar -xzf speedcam-deploy.tar.gz + +# Artifact Registry 인증 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 환경 변수 로드 +source env/hosts.env + +# Docker Compose 실행 +docker compose -f compose/docker-compose.db.yml up -d + +# 로그 확인 +docker logs speedcam-mysql +docker logs speedcam-mysqld-exporter +docker logs speedcam-promtail +docker logs speedcam-cadvisor + +# 헬스체크 +docker ps +``` + +#### 3) MySQL 초기화 확인 + +```bash +# MySQL 접속 테스트 +docker exec -it speedcam-mysql mysql -u sa -p + +# SQL 실행 (MySQL 콘솔에서) +SHOW DATABASES; +USE speedcam; +SHOW TABLES; +EXIT; +``` + +#### 4) SSH 종료 + +```bash +exit +``` + +--- + +### 7.2 speedcam-mq 배포 + +#### 1) SCP로 파일 전송 + +```bash +gcloud compute scp speedcam-deploy.tar.gz speedcam-mq:~/ --zone=asia-northeast3-a +``` + +#### 2) SSH 접속 및 배포 + +```bash +gcloud compute ssh speedcam-mq --zone=asia-northeast3-a +``` + +**인스턴스 내부 작업:** + +```bash +# 압축 해제 +tar -xzf speedcam-deploy.tar.gz + +# Artifact Registry 인증 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 환경 변수 로드 +source env/hosts.env + +# Docker Compose 실행 +docker compose -f compose/docker-compose.mq.yml up -d + +# 로그 확인 +docker logs speedcam-rabbitmq +docker logs speedcam-promtail +docker logs speedcam-cadvisor + +# 헬스체크 +docker ps +``` + +#### 3) RabbitMQ 웹 UI 확인 + +```bash +# 브라우저에서 접속 +# http://:15672 +# 로그인: sa / +``` + +#### 4) SSH 종료 + +```bash +exit +``` + +--- + +### 7.3 speedcam-mon 배포 + +#### 1) SCP로 파일 전송 + +```bash +gcloud compute scp speedcam-deploy.tar.gz speedcam-mon:~/ --zone=asia-northeast3-a +``` + +#### 2) SSH 접속 및 배포 + +```bash +gcloud compute ssh speedcam-mon --zone=asia-northeast3-a +``` + +**인스턴스 내부 작업:** + +```bash +# 압축 해제 +tar -xzf speedcam-deploy.tar.gz + +# Artifact Registry 인증 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 환경 변수 로드 +source env/hosts.env + +# Docker Compose 실행 +docker compose -f compose/docker-compose.mon.yml up -d + +# 로그 확인 +docker logs speedcam-prometheus +docker logs speedcam-grafana +docker logs speedcam-loki +docker logs speedcam-jaeger +docker logs speedcam-otel-collector +docker logs speedcam-celery-exporter +docker logs speedcam-promtail +docker logs speedcam-cadvisor + +# 헬스체크 +docker ps +``` + +#### 3) Grafana 웹 UI 확인 + +```bash +# 브라우저에서 접속 +# http://:3000 +# 로그인: admin / +``` + +#### 4) SSH 종료 + +```bash +exit +``` + +--- + +### 7.4 speedcam-app 배포 + +#### 1) SCP로 파일 전송 + +```bash +gcloud compute scp speedcam-deploy.tar.gz speedcam-app:~/ --zone=asia-northeast3-a +``` + +#### 2) SSH 접속 및 배포 + +```bash +gcloud compute ssh speedcam-app --zone=asia-northeast3-a +``` + +**인스턴스 내부 작업:** + +```bash +# 압축 해제 +tar -xzf speedcam-deploy.tar.gz + +# Artifact Registry 인증 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 환경 변수 로드 +source env/hosts.env + +# Docker Compose 실행 +docker compose -f compose/docker-compose.app.yml up -d + +# 로그 확인 +docker logs speedcam-traefik +docker logs speedcam-main +docker logs speedcam-flower +docker logs speedcam-promtail +docker logs speedcam-cadvisor + +# 헬스체크 +docker ps +``` + +#### 3) API 헬스체크 + +```bash +# Django API 헬스체크 +curl http://localhost:8000/health/ + +# Flower 접속 확인 (브라우저) +# http://:5555 +``` + +#### 4) SSH 종료 + +```bash +exit +``` + +--- + +### 7.5 speedcam-ocr 배포 + +#### 1) SCP로 파일 전송 + +```bash +gcloud compute scp speedcam-deploy.tar.gz speedcam-ocr:~/ --zone=asia-northeast3-a +``` + +#### 2) SSH 접속 및 배포 + +```bash +gcloud compute ssh speedcam-ocr --zone=asia-northeast3-a +``` + +**인스턴스 내부 작업:** + +```bash +# 압축 해제 +tar -xzf speedcam-deploy.tar.gz + +# Artifact Registry 인증 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 환경 변수 로드 +source env/hosts.env + +# Docker Compose 실행 +docker compose -f compose/docker-compose.ocr.yml up -d + +# 로그 확인 +docker logs speedcam-ocr +docker logs speedcam-promtail +docker logs speedcam-cadvisor + +# 헬스체크 +docker ps +``` + +#### 3) Worker 상태 확인 + +```bash +# Flower에서 OCR Worker 확인 +# http://:5555 +# Workers 탭에서 "ocr@" 활성화 확인 +``` + +#### 4) SSH 종료 + +```bash +exit +``` + +--- + +### 7.6 speedcam-alert 배포 + +#### 1) SCP로 파일 전송 + +```bash +gcloud compute scp speedcam-deploy.tar.gz speedcam-alert:~/ --zone=asia-northeast3-a +``` + +#### 2) SSH 접속 및 배포 + +```bash +gcloud compute ssh speedcam-alert --zone=asia-northeast3-a +``` + +**인스턴스 내부 작업:** + +```bash +# 압축 해제 +tar -xzf speedcam-deploy.tar.gz + +# Artifact Registry 인증 +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 환경 변수 로드 +source env/hosts.env + +# Docker Compose 실행 +docker compose -f compose/docker-compose.alert.yml up -d + +# 로그 확인 +docker logs speedcam-alert +docker logs speedcam-promtail +docker logs speedcam-cadvisor + +# 헬스체크 +docker ps +``` + +#### 3) Worker 상태 확인 + +```bash +# Flower에서 Alert Worker 확인 +# http://:5555 +# Workers 탭에서 "alert@" 활성화 확인 +``` + +#### 4) SSH 종료 + +```bash +exit +``` + +--- + +## 8. 헬스체크 및 검증 + +### 8.1 MySQL 헬스체크 + +```bash +# speedcam-db에 SSH 접속 +gcloud compute ssh speedcam-db --zone=asia-northeast3-a + +# MySQL 접속 테스트 +docker exec -it speedcam-mysql mysql -u sa -p -e "SELECT 1;" + +# 데이터베이스 확인 +docker exec -it speedcam-mysql mysql -u sa -p -e "SHOW DATABASES;" + +# MySQL Exporter 메트릭 확인 +curl http://localhost:9104/metrics | grep mysql_up +# 출력: mysql_up 1 +``` + +### 8.2 RabbitMQ 헬스체크 + +```bash +# speedcam-mq에 SSH 접속 +gcloud compute ssh speedcam-mq --zone=asia-northeast3-a + +# RabbitMQ 상태 확인 +docker exec speedcam-rabbitmq rabbitmq-diagnostics status + +# 플러그인 확인 (MQTT, Prometheus 활성화 확인) +docker exec speedcam-rabbitmq rabbitmq-plugins list + +# 웹 UI 접속 +# http://:15672 +``` + +### 8.3 Django API 헬스체크 + +```bash +# speedcam-app에 SSH 접속 +gcloud compute ssh speedcam-app --zone=asia-northeast3-a + +# API 헬스체크 엔드포인트 +curl http://localhost:8000/health/ +# 기대 출력: {"status": "ok"} + +# Django 마이그레이션 확인 +docker exec speedcam-main python manage.py showmigrations + +# Django 관리자 생성 (필요시) +docker exec -it speedcam-main python manage.py createsuperuser +``` + +### 8.4 Traefik 헬스체크 + +```bash +# speedcam-app에 SSH 접속 +gcloud compute ssh speedcam-app --zone=asia-northeast3-a + +# Traefik 대시보드 확인 +curl http://localhost:8080/api/http/routers +curl http://localhost:8080/api/http/services + +# 웹 브라우저에서 +# http://:8080/dashboard/ +``` + +### 8.5 Celery Workers 헬스체크 + +```bash +# Flower 웹 UI 접속 +# http://:5555 + +# Workers 탭 확인: +# - ocr@speedcam-ocr (4 concurrency) +# - alert@speedcam-alert (100 concurrency) +``` + +### 8.6 Prometheus 헬스체크 + +```bash +# speedcam-mon에 SSH 접속 +gcloud compute ssh speedcam-mon --zone=asia-northeast3-a + +# Prometheus 타겟 확인 +curl http://localhost:9090/api/v1/targets | jq . + +# 웹 브라우저에서 +# http://:9090 +# Status → Targets → 모든 타겟 UP 확인 +``` + +### 8.7 Grafana 헬스체크 + +```bash +# 웹 브라우저에서 접속 +# http://:3000 +# 로그인: admin / + +# 데이터 소스 확인: +# Configuration → Data Sources +# - Prometheus (http://localhost:9090) +# - Loki (http://localhost:3100) +# - Jaeger (http://localhost:16686) +``` + +### 8.8 전체 시스템 헬스체크 스크립트 + +**로컬에서 실행:** + +```bash +#!/bin/bash +# health-check.sh + +INSTANCES=( + "speedcam-db:10.178.0.11" + "speedcam-mq:10.178.0.12" + "speedcam-app:10.178.0.13" + "speedcam-ocr:10.178.0.14" + "speedcam-alert:10.178.0.15" + "speedcam-mon:10.178.0.20" +) + +echo "=========================================" +echo " SpeedCam 전체 헬스체크" +echo "=========================================" + +for instance in "${INSTANCES[@]}"; do + name="${instance%%:*}" + ip="${instance##*:}" + + echo "" + echo "[$name] 컨테이너 상태 확인..." + gcloud compute ssh $name --zone=asia-northeast3-a --command="docker ps --format 'table {{.Names}}\t{{.Status}}'" +done + +echo "" +echo "=========================================" +echo " 완료" +echo "=========================================" +``` + +**실행:** + +```bash +chmod +x health-check.sh +./health-check.sh +``` + +--- + +## 9. 롤백 절차 + +### 9.1 특정 이미지 버전으로 롤백 + +```bash +# 1. 롤백할 이미지 태그 확인 +gcloud artifacts docker images list ${ARTIFACT_REGISTRY}/speedcam-main --include-tags + +# 2. 인스턴스에 SSH 접속 (예: speedcam-app) +gcloud compute ssh speedcam-app --zone=asia-northeast3-a + +# 3. 환경 변수 수정 (이미지 태그 변경) +nano env/hosts.env +# export ARTIFACT_REGISTRY=asia-northeast3-docker.pkg.dev//speedcam:20260207-120000 + +# 4. Docker Compose 재시작 +source env/hosts.env +docker compose -f compose/docker-compose.app.yml pull +docker compose -f compose/docker-compose.app.yml up -d + +# 5. 로그 확인 +docker logs speedcam-main +``` + +### 9.2 전체 서비스 롤백 + +```bash +# 역순으로 롤백: ALERT → OCR → APP → MON → MQ → DB +# 각 인스턴스에서: + +docker compose -f compose/docker-compose..yml down +docker compose -f compose/docker-compose..yml up -d +``` + +### 9.3 데이터 백업 (롤백 전) + +```bash +# MySQL 백업 +gcloud compute ssh speedcam-db --zone=asia-northeast3-a +docker exec speedcam-mysql mysqldump -u sa -p --all-databases > /tmp/backup-$(date +%Y%m%d).sql + +# 백업 파일 다운로드 +gcloud compute scp speedcam-db:/tmp/backup-*.sql ~/backups/ --zone=asia-northeast3-a +``` + +--- + +## 10. 이미지 버전 동기화 체크리스트 + +### 10.1 모니터링 이미지 버전 확인 + +**backend 레포지토리와 depoly 레포지토리의 모니터링 이미지 버전이 일치해야 합니다.** + +#### backend/docker/monitoring/docker-compose.yml + +```bash +# backend 레포지토리에서 확인 +cd +grep 'image:' docker/monitoring/docker-compose.yml +``` + +**예시 출력:** +```yaml +prometheus: prom/prometheus:v2.51.2 +grafana: grafana/grafana:10.4.2 +loki: grafana/loki:2.9.6 +promtail: grafana/promtail:2.9.6 +jaeger: jaegertracing/all-in-one:1.57 +otel-collector: otel/opentelemetry-collector-contrib:0.98.0 +cadvisor: gcr.io/cadvisor/cadvisor:v0.49.1 +mysqld-exporter: prom/mysqld-exporter:v0.15.1 +celery-exporter: danihodovic/celery-exporter:0.10.3 +``` + +#### depoly/compose/docker-compose.mon.yml + +```bash +# depoly 레포지토리에서 확인 +cd /path/to/depoly +grep 'image:' compose/docker-compose.mon.yml +grep 'image:' compose/docker-compose.db.yml +grep 'image:' compose/docker-compose.mq.yml +grep 'image:' compose/docker-compose.app.yml +grep 'image:' compose/docker-compose.ocr.yml +grep 'image:' compose/docker-compose.alert.yml +``` + +### 10.2 버전 불일치 해결 + +**버전이 다르면 depoly 레포지토리를 backend 기준으로 업데이트:** + +```bash +# depoly 레포지토리에서 +nano compose/docker-compose.mon.yml +# 이미지 버전을 backend와 동일하게 수정 + +# 변경사항 커밋 +git add compose/docker-compose.mon.yml +git commit -m "chore: sync monitoring image versions with backend" +git push origin main +``` + +### 10.3 프로덕션 배포 전 체크리스트 + +- [ ] backend 레포지토리 최신 커밋 확인 +- [ ] depoly 레포지토리 최신 커밋 확인 +- [ ] 모니터링 이미지 버전 동기화 확인 +- [ ] backend 이미지 빌드 및 푸시 완료 +- [ ] env 파일 모두 설정 완료 (hosts.env, backend.env, mysql.env, rabbitmq.env) +- [ ] credentials 디렉토리 생성 완료 (GCS, Firebase JSON) +- [ ] MySQL Exporter .my.cnf 생성 완료 +- [ ] setup-env.sh 실행 완료 (Traefik, Prometheus, Promtail 설정 생성) +- [ ] config/mysql/init.sql 확인 (최신 스키마) + +--- + +## 11. 트러블슈팅 + +### 11.1 MySQL 컨테이너 시작 실패 + +**증상:** +``` +speedcam-mysql exited with code 1 +``` + +**해결:** + +```bash +# 로그 확인 +docker logs speedcam-mysql + +# 일반적인 원인: +# 1. 비밀번호 환경 변수 누락 → env/mysql.env 확인 +# 2. init.sql 문법 오류 → config/mysql/init.sql 확인 +# 3. 볼륨 권한 문제 → sudo chown -R 999:999 /var/lib/docker/volumes/mysql_data +``` + +### 11.2 RabbitMQ 플러그인 활성화 실패 + +**증상:** +``` +MQTT 포트 1883 연결 실패 +``` + +**해결:** + +```bash +# 컨테이너 재시작 +docker restart speedcam-rabbitmq + +# 플러그인 수동 활성화 +docker exec speedcam-rabbitmq rabbitmq-plugins enable rabbitmq_mqtt rabbitmq_prometheus + +# 확인 +docker exec speedcam-rabbitmq rabbitmq-plugins list +``` + +### 11.3 Artifact Registry 인증 실패 + +**증상:** +``` +Error response from daemon: unauthorized: You don't have the needed permissions +``` + +**해결:** + +```bash +# gcloud 재인증 +gcloud auth login +gcloud auth configure-docker asia-northeast3-docker.pkg.dev + +# 서비스 계정 키 사용 (선택사항) +gcloud auth activate-service-account --key-file= +``` + +### 11.4 Django API 500 에러 + +**증상:** +``` +curl http://localhost:8000/health/ +{"detail": "Internal Server Error"} +``` + +**해결:** + +```bash +# Django 로그 확인 +docker logs speedcam-main + +# 일반적인 원인: +# 1. MySQL 연결 실패 → backend.env의 DB_HOST 확인 +# 2. 마이그레이션 미실행 → docker exec speedcam-main python manage.py migrate +# 3. Credentials 누락 → config/credentials/*.json 확인 +# 4. RabbitMQ 연결 실패 → backend.env의 CELERY_BROKER_URL 확인 +``` + +### 11.5 Prometheus 타겟 DOWN + +**증상:** +``` +Prometheus 웹 UI에서 타겟이 DOWN 상태 +``` + +**해결:** + +```bash +# Prometheus 설정 확인 +cat config/monitoring/prometheus/prometheus.yml + +# 타겟 인스턴스에서 Exporter 상태 확인 +docker ps | grep exporter +docker logs speedcam-mysqld-exporter +docker logs speedcam-cadvisor + +# 네트워크 연결 확인 +curl http://<타겟 IP>:9104/metrics # mysqld-exporter +curl http://<타겟 IP>:8080/metrics # cadvisor +``` + +### 11.6 Celery Worker 등록 안 됨 + +**증상:** +``` +Flower에서 Worker가 보이지 않음 +``` + +**해결:** + +```bash +# Worker 로그 확인 +docker logs speedcam-ocr +docker logs speedcam-alert + +# 일반적인 원인: +# 1. RabbitMQ 연결 실패 → backend.env의 CELERY_BROKER_URL 확인 +# 2. Worker 컨테이너 종료 → docker ps 확인 +# 3. Celery 설정 오류 → docker logs에서 traceback 확인 + +# Worker 재시작 +docker restart speedcam-ocr +docker restart speedcam-alert +``` + +### 11.7 Traefik HTTPS 인증서 발급 실패 + +**증상:** +``` +도메인 접속 시 "Your connection is not private" 경고 +``` + +**해결:** + +```bash +# Traefik 로그 확인 +docker logs speedcam-traefik + +# 일반적인 원인: +# 1. DNS A 레코드 미설정 → 도메인이 speedcam-app 외부 IP를 가리키는지 확인 +# 2. 80 포트 막힘 → 방화벽 규칙 확인 (Let's Encrypt HTTP-01 Challenge) +# 3. ACME 이메일 미설정 → config/traefik/traefik.yml 확인 + +# 인증서 수동 재발급 +docker exec speedcam-traefik rm -rf /etc/traefik/certs/* +docker restart speedcam-traefik +``` + +--- + +## 부록: 도메인 모드 DNS 설정 + +**도메인을 사용하는 경우 (예: autonotify.store):** + +| 서브도메인 | 타입 | 값 | 설명 | +|-----------|------|-----|------| +| api.autonotify.store | A | speedcam-app 외부 IP | Django API | +| flower.autonotify.store | A | speedcam-app 외부 IP | Celery 모니터링 | +| grafana.autonotify.store | A | speedcam-app 외부 IP | Grafana (Traefik으로 프록시) | +| rabbitmq.autonotify.store | A | speedcam-app 외부 IP | RabbitMQ 관리 (Traefik으로 프록시) | +| traefik.autonotify.store | A | speedcam-app 외부 IP | Traefik 대시보드 | + +**Traefik이 모든 서브도메인을 내부 서비스로 라우팅합니다.** + +--- + +## 결론 + +이 가이드를 통해 6개의 GCE 인스턴스에 SpeedCam 프로젝트를 수동으로 배포할 수 있습니다. 배포 후 반드시 헬스체크를 수행하여 모든 서비스가 정상 작동하는지 확인하세요. + +**다음 단계:** +- CI/CD 파이프라인 구축 (GitHub Actions) +- 자동 배포 스크립트 작성 +- 모니터링 대시보드 커스터마이징 +- 백업 자동화 (MySQL, 설정 파일) + +**문의:** +- Backend: +- Deploy: diff --git a/env/backend.env.example b/env/backend.env.example new file mode 100644 index 0000000..de751d0 --- /dev/null +++ b/env/backend.env.example @@ -0,0 +1,54 @@ +# =========================================== +# SpeedCam Backend 환경 변수 +# =========================================== +# 사용법: 이 파일을 backend.env로 복사 후 수정 +# cp backend.env.example backend.env + +# Django +SECRET_KEY= +DJANGO_SETTINGS_MODULE=config.settings.prod +DEBUG=False + +# Database -- hosts.env의 DB_HOST IP로 교체 +DB_HOST= +DB_PORT=3306 +DB_USER=sa +DB_PASSWORD= +DB_NAME=speedcam +DB_NAME_VEHICLES=speedcam_vehicles +DB_NAME_DETECTIONS=speedcam_detections +DB_NAME_NOTIFICATIONS=speedcam_notifications + +# RabbitMQ / Celery -- hosts.env의 MQ_HOST IP로 교체 +CELERY_BROKER_URL=amqp://sa:@:5672// +RABBITMQ_HOST= +MQTT_PORT=1883 +MQTT_USER=sa +MQTT_PASS= + +# GCS / Firebase +GOOGLE_APPLICATION_CREDENTIALS=/app/credentials/gcp-cloud-storage.json +FIREBASE_CREDENTIALS=/app/credentials/firebase-service-account.json + +# Workers +OCR_CONCURRENCY=4 +ALERT_CONCURRENCY=100 +OCR_MOCK=false +FCM_MOCK=false + +# Gunicorn +GUNICORN_WORKERS=4 +GUNICORN_THREADS=2 + +# Logging +LOG_LEVEL=info + +# CORS -- 프론트엔드 도메인으로 교체 (도메인 없으면 프론트엔드 IP) +CORS_ALLOWED_ORIGINS=http://localhost:3000 + +# OpenTelemetry -- hosts.env의 MON_HOST IP로 교체 +OTEL_EXPORTER_OTLP_ENDPOINT=http://:4317 +OTEL_EXPORTER_OTLP_PROTOCOL=grpc +OTEL_RESOURCE_ATTRIBUTES=service.namespace=speedcam,deployment.environment=prod +OTEL_TRACES_SAMPLER=parentbased_tracealways +OTEL_PYTHON_LOG_CORRELATION=true diff --git a/env/hosts.env.example b/env/hosts.env.example new file mode 100644 index 0000000..f5ca3b0 --- /dev/null +++ b/env/hosts.env.example @@ -0,0 +1,34 @@ +# =========================================== +# 인스턴스 내부 IP 설정 +# =========================================== +# GCP 인스턴스 생성 후 내부 IP를 확인하여 입력 +# gcloud compute instances list --filter="name~speedcam" \ +# --format="table(name, networkInterfaces[0].networkIP)" + +export DB_HOST=10.178.0.11 +export MQ_HOST=10.178.0.12 +export APP_HOST=10.178.0.13 +export OCR_HOST=10.178.0.14 +export ALERT_HOST=10.178.0.15 +export MON_HOST=10.178.0.20 + +# Artifact Registry 경로 +export ARTIFACT_REGISTRY=asia-northeast3-docker.pkg.dev//speedcam + +# RabbitMQ 비밀번호 (celery-exporter에서 사용) +export RABBITMQ_PASSWORD= + +# Grafana 비밀번호 +export GRAFANA_PASSWORD= + +# =========================================== +# 도메인 설정 (선택사항) +# =========================================== +# 도메인이 없으면 비워두기 → IP 모드로 동작 (HTTP only) +# 도메인이 있으면 입력 → 도메인 모드로 동작 (HTTPS + Let's Encrypt) +# 예: export DOMAIN=autonotify.store +export DOMAIN= + +# 도메인 모드 전용 설정 +export ACME_EMAIL=your-email@example.com +export TRAEFIK_AUTH_USER=admin:$$2y$$05$$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/env/mysql.env.example b/env/mysql.env.example new file mode 100644 index 0000000..0081ee8 --- /dev/null +++ b/env/mysql.env.example @@ -0,0 +1,7 @@ +# =========================================== +# MySQL 환경 변수 +# =========================================== +MYSQL_ROOT_PASSWORD= +MYSQL_USER=sa +MYSQL_PASSWORD= +MYSQL_DATABASE=speedcam diff --git a/env/rabbitmq.env.example b/env/rabbitmq.env.example new file mode 100644 index 0000000..c9f0238 --- /dev/null +++ b/env/rabbitmq.env.example @@ -0,0 +1,5 @@ +# =========================================== +# RabbitMQ 환경 변수 +# =========================================== +RABBITMQ_DEFAULT_USER=sa +RABBITMQ_DEFAULT_PASS= diff --git a/scripts/deploy-all.sh b/scripts/deploy-all.sh new file mode 100755 index 0000000..416b803 --- /dev/null +++ b/scripts/deploy-all.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# =========================================== +# 전체 인스턴스 배포 스크립트 +# =========================================== +# 사용법: ./scripts/deploy-all.sh +# 예시: ./scripts/deploy-all.sh asia-northeast3-a +# +# 기능: +# - 모든 인스턴스를 순서대로 배포 (DB → MQ → MON → APP → OCR → ALERT) +# - 각 배포 사이에 대기 시간을 두어 서비스가 안정화되도록 함 +# - 배포 실패 시에도 계속 진행하고 최종 결과 요약 제공 +# =========================================== + +set -euo pipefail + +# ===== 색상 정의 ===== +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ===== 함수: 색상 출력 ===== +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# ===== 인자 검증 ===== +if [ $# -ne 1 ]; then + print_error "인자가 잘못되었습니다." + echo "사용법: $0 " + echo "예시: $0 asia-northeast3-a" + exit 1 +fi + +ZONE="$1" + +# ===== 디렉토리 설정 ===== +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_SCRIPT="${SCRIPT_DIR}/deploy-instance.sh" + +# deploy-instance.sh 존재 확인 +if [ ! -f "$DEPLOY_SCRIPT" ]; then + print_error "deploy-instance.sh를 찾을 수 없습니다: ${DEPLOY_SCRIPT}" + exit 1 +fi + +# 실행 권한 확인 +if [ ! -x "$DEPLOY_SCRIPT" ]; then + print_warning "deploy-instance.sh에 실행 권한 부여 중..." + chmod +x "$DEPLOY_SCRIPT" +fi + +print_info "==========================================" +print_info "전체 인스턴스 배포 시작" +print_info "==========================================" +print_info "존(Zone): ${ZONE}" +print_info "배포 순서: DB → MQ → MON → APP → OCR → ALERT" +echo "" + +# ===== 배포 대상 인스턴스 목록 (순서 중요) ===== +declare -a INSTANCES=( + "speedcam-db" + "speedcam-mq" + "speedcam-mon" + "speedcam-app" + "speedcam-ocr" + "speedcam-alert" +) + +# ===== 대기 시간 설정 (초) ===== +declare -A WAIT_TIME=( + ["speedcam-db"]=30 + ["speedcam-mq"]=20 + ["speedcam-mon"]=20 + ["speedcam-app"]=15 + ["speedcam-ocr"]=15 + ["speedcam-alert"]=10 +) + +# ===== 배포 결과 추적 ===== +declare -A DEPLOY_RESULTS=() + +# ===== 각 인스턴스 배포 ===== +for INSTANCE in "${INSTANCES[@]}"; do + print_info "==========================================" + print_info "배포 시작: ${INSTANCE}" + print_info "==========================================" + + # deploy-instance.sh 실행 (실패해도 계속 진행) + if "$DEPLOY_SCRIPT" "$INSTANCE" "$ZONE"; then + DEPLOY_RESULTS["$INSTANCE"]="SUCCESS" + print_success "${INSTANCE} 배포 성공" + + # 대기 시간 + WAIT_SEC=${WAIT_TIME[$INSTANCE]} + print_info "${INSTANCE} 서비스 안정화 대기 중... (${WAIT_SEC}초)" + sleep "$WAIT_SEC" + else + DEPLOY_RESULTS["$INSTANCE"]="FAILED" + print_error "${INSTANCE} 배포 실패" + print_warning "다음 인스턴스 배포를 계속합니다..." + fi + + echo "" +done + +# ===== 배포 결과 요약 ===== +print_info "==========================================" +print_info "전체 배포 결과 요약" +print_info "==========================================" + +SUCCESS_COUNT=0 +FAILED_COUNT=0 + +for INSTANCE in "${INSTANCES[@]}"; do + RESULT="${DEPLOY_RESULTS[$INSTANCE]}" + if [ "$RESULT" = "SUCCESS" ]; then + echo -e "${GREEN}✓${NC} ${INSTANCE}: ${GREEN}성공${NC}" + ((SUCCESS_COUNT++)) + else + echo -e "${RED}✗${NC} ${INSTANCE}: ${RED}실패${NC}" + ((FAILED_COUNT++)) + fi +done + +echo "" +print_info "성공: ${SUCCESS_COUNT}개 / 실패: ${FAILED_COUNT}개" + +# ===== 최종 상태 코드 반환 ===== +if [ "$FAILED_COUNT" -gt 0 ]; then + print_error "일부 인스턴스 배포 실패" + exit 1 +else + print_success "==========================================" + print_success "모든 인스턴스 배포 완료!" + print_success "==========================================" + print_info "서비스 상태 확인:" + echo " gcloud compute ssh speedcam-app --zone=${ZONE} --command='curl -s http://localhost/health'" + echo " gcloud compute ssh speedcam-mon --zone=${ZONE} --command='docker ps'" + exit 0 +fi diff --git a/scripts/deploy-instance.sh b/scripts/deploy-instance.sh new file mode 100755 index 0000000..665f572 --- /dev/null +++ b/scripts/deploy-instance.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# =========================================== +# 단일 인스턴스 배포 스크립트 +# =========================================== +# 사용법: ./scripts/deploy-instance.sh +# 예시: ./scripts/deploy-instance.sh speedcam-app asia-northeast3-a +# +# 기능: +# - 인스턴스 이름을 기반으로 docker-compose 파일 자동 매핑 +# - depoly/ 디렉토리 전체를 인스턴스에 SCP +# - 인스턴스에 SSH 접속하여 Docker 컨테이너 배포 +# =========================================== + +set -euo pipefail + +# ===== 색상 정의 ===== +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ===== 함수: 색상 출력 ===== +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# ===== 인자 검증 ===== +if [ $# -ne 2 ]; then + print_error "인자가 잘못되었습니다." + echo "사용법: $0 " + echo "예시: $0 speedcam-app asia-northeast3-a" + exit 1 +fi + +INSTANCE_NAME="$1" +ZONE="$2" + +# ===== 디렉토리 설정 ===== +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_DIR="$(dirname "$SCRIPT_DIR")" + +print_info "배포 디렉토리: ${DEPLOY_DIR}" +print_info "대상 인스턴스: ${INSTANCE_NAME}" +print_info "존(Zone): ${ZONE}" + +# ===== 인스턴스 이름 → 역할(role) 매핑 ===== +case "$INSTANCE_NAME" in + speedcam-app) + ROLE="app" + NEEDS_PULL=true + ;; + speedcam-db) + ROLE="db" + NEEDS_PULL=false + ;; + speedcam-mq) + ROLE="mq" + NEEDS_PULL=false + ;; + speedcam-mon) + ROLE="mon" + NEEDS_PULL=false + ;; + speedcam-ocr) + ROLE="ocr" + NEEDS_PULL=true + ;; + speedcam-alert) + ROLE="alert" + NEEDS_PULL=true + ;; + *) + print_error "알 수 없는 인스턴스 이름: ${INSTANCE_NAME}" + echo "지원되는 인스턴스: speedcam-app, speedcam-db, speedcam-mq, speedcam-mon, speedcam-ocr, speedcam-alert" + exit 1 + ;; +esac + +COMPOSE_FILE="docker-compose.${ROLE}.yml" +print_info "역할(ROLE): ${ROLE}" +print_info "Compose 파일: compose/${COMPOSE_FILE}" + +# ===== Compose 파일 존재 확인 ===== +if [ ! -f "${DEPLOY_DIR}/compose/${COMPOSE_FILE}" ]; then + print_error "Compose 파일이 존재하지 않습니다: ${DEPLOY_DIR}/compose/${COMPOSE_FILE}" + exit 1 +fi + +# ===== 인스턴스 존재 확인 ===== +print_info "인스턴스 존재 여부 확인 중..." +if ! gcloud compute instances describe "$INSTANCE_NAME" --zone="$ZONE" &>/dev/null; then + print_error "인스턴스를 찾을 수 없습니다: ${INSTANCE_NAME} (zone: ${ZONE})" + exit 1 +fi +print_success "인스턴스 확인 완료" + +# ===== 1. SCP로 depoly/ 디렉토리 전송 ===== +print_info "==========================================" +print_info "Step 1: depoly/ 디렉토리 전송 중..." +print_info "==========================================" + +gcloud compute scp --recurse \ + --zone="$ZONE" \ + "${DEPLOY_DIR}" \ + "${INSTANCE_NAME}:~/" \ + || { print_error "SCP 전송 실패"; exit 1; } + +print_success "depoly/ 디렉토리 전송 완료" + +# ===== 2. SSH로 배포 명령 실행 ===== +print_info "==========================================" +print_info "Step 2: 인스턴스에서 배포 실행 중..." +print_info "==========================================" + +# 배포 스크립트 생성 (heredoc 사용) +DEPLOY_SCRIPT=$(cat </dev/null || \ + sed -i '' "s/placeholder@example.com/${ACME_EMAIL}/" \ + "${DEPLOY_DIR}/config/traefik/traefik.yml" + rm -f "${DEPLOY_DIR}/config/traefik/traefik.yml.bak" + + envsubst '${DOMAIN} ${MON_HOST} ${MQ_HOST} ${TRAEFIK_AUTH_USER}' \ + < "${DEPLOY_DIR}/config/traefik/dynamic_conf.domain.yml.template" \ + > "${DEPLOY_DIR}/config/traefik/dynamic_conf.yml" + echo " → config/traefik/dynamic_conf.yml (도메인 모드)" +else + echo "[1/3] Traefik 동적 설정 생성 (IP 모드)" + cp "${DEPLOY_DIR}/config/traefik/dynamic_conf.ip.yml" \ + "${DEPLOY_DIR}/config/traefik/dynamic_conf.yml" + echo " → config/traefik/dynamic_conf.yml (IP 모드)" +fi + +# ===== 2. Prometheus 설정 ===== +echo "[2/3] Prometheus 설정 생성..." +envsubst '${DB_HOST} ${MQ_HOST} ${APP_HOST} ${OCR_HOST} ${ALERT_HOST}' \ + < "${DEPLOY_DIR}/config/monitoring/prometheus/prometheus.yml.template" \ + > "${DEPLOY_DIR}/config/monitoring/prometheus/prometheus.yml" +echo " → config/monitoring/prometheus/prometheus.yml" + +# ===== 3. Promtail 설정 ===== +echo "[3/3] Promtail 설정 생성..." +envsubst '${MON_HOST}' \ + < "${DEPLOY_DIR}/config/monitoring/promtail/promtail-config.yml.template" \ + > "${DEPLOY_DIR}/config/monitoring/promtail/promtail-config.yml" +echo " → config/monitoring/promtail/promtail-config.yml" + +echo "" +echo "=============================================" +echo " 완료!" +echo "=============================================" +echo "" +echo " 아래 파일은 수동으로 설정하세요:" +echo " - env/backend.env (env/backend.env.example 참고)" +echo " - env/mysql.env (env/mysql.env.example 참고)" +echo " - env/rabbitmq.env (env/rabbitmq.env.example 참고)" +echo " - config/monitoring/mysqld-exporter/.my.cnf" +echo "" +if [ -n "${DOMAIN:-}" ]; then + echo " 도메인 DNS 설정:" + echo " A 레코드를 speedcam-app 외부 IP로 지정하세요:" + echo " - api.${DOMAIN}" + echo " - flower.${DOMAIN}" + echo " - grafana.${DOMAIN}" + echo " - rabbitmq.${DOMAIN}" + echo " - traefik.${DOMAIN}" +else + echo " IP 모드로 설정됨:" + echo " - API 접근: http://" + echo " - 관리 도구는 VPC 내부 IP로 직접 접근" + echo " - 도메인 추가 시: env/hosts.env에서 DOMAIN 설정 후 재실행" +fi +echo "=============================================" diff --git a/traefik/dynamic_conf.yml b/traefik/dynamic_conf.yml deleted file mode 100644 index 27d7e45..0000000 --- a/traefik/dynamic_conf.yml +++ /dev/null @@ -1,63 +0,0 @@ -http: - routers: - django-app-router: - rule: "Host(`api.autonotify.store`) && PathPrefix(`/api/v1`)" - entryPoints: - - websecure - service: django-app-service - tls: - certResolver: letsencrypt - middlewares: - - cors - - rate-limit - - portainer-router: - rule: "Host(`portainer.autonotify.store`)" - entryPoints: - - websecure - service: portainer-service - middlewares: - - dashboard-auth # 포테이너 대시보드 인증 - - services: - django-app-service: - loadBalancer: - servers: - - url: "http://backend:8000" - - portainer-service: - loadBalancer: - servers: - - url: "http://portainer:9000" - - middlewares: - cors: - headers: - accessControlAllowOriginList: - - "*" - accessControlAllowMethods: - - GET - - POST - - PUT - - PATCH - - DELETE - - OPTIONS - accessControlAllowHeaders: - - "*" - accessControlAllowCredentials: true - addVaryHeader: true - - rate-limit: - rateLimit: - average: 50 - burst: 100 - - auth: - basicAuth: - users: - - "autonotify:$2y$05$nk3Iz..3eY6AhbgWcPJz/.hv2Wvbnq6ddgSjrnoLsxDvMPWdm4Jga" - - dashboard-auth: - basicAuth: - users: - - "autonotify:$2y$05$nk3Iz..3eY6AhbgWcPJz/.hv2Wvbnq6ddgSjrnoLsxDvMPWdm4Jga" diff --git a/traefik/traefik.yml b/traefik/traefik.yml deleted file mode 100644 index 9b6ad5a..0000000 --- a/traefik/traefik.yml +++ /dev/null @@ -1,51 +0,0 @@ -entryPoints: - web: - address: ":80" - - websecure: - address: ":443" - - dashboard: - address: ":8088" - -api: - dashboard: true - insecure: false - -log: - level: INFO - -accessLog: {} - -providers: - docker: - exposedByDefault: false - network: web - - file: - filename: "/etc/traefik/dynamic_conf.yml" - -certificatesResolvers: - letsencrypt: - acme: - email: sanghun@gmail.com - storage: /etc/traefik/acme.json - httpChallenge: - entryPoint: web - -http: - middlewares: - redirect-to-https: - redirectScheme: - scheme: https - permanent: true - - routers: - redirect-all: - rule: "PathPrefix(`/`)" - entryPoints: - - web - middlewares: - - redirect-to-https - service: noop@internal - priority: 1