Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5a12a82
[Chore] 테스트 워크플로 주석을 실제 배포 흐름에 맞게 고친다
KiSeungMin Sep 21, 2026
288da42
Merge pull request #334 from AI-SIP/chore/require-test-before-merge
KiSeungMin Sep 21, 2026
548e341
[Chore] 모니터링 알림을 디스코드로 연결하고 대시보드의 빈 패널과 틀린 값을 고친다
KiSeungMin Sep 21, 2026
90ab6a5
Merge pull request #336 from AI-SIP/chore/monitoring-alert-dashboard
KiSeungMin Sep 21, 2026
00630ac
[Feat] 스모크 테스트가 토큰 갱신과 오답노트 등록까지 확인한다
KiSeungMin Sep 21, 2026
f9d5a0c
[Feat] prod 배포가 nginx 전환 전에 새 컨테이너를 확인한다
KiSeungMin Sep 21, 2026
b5418ce
Merge pull request #337 from AI-SIP/feat/smoke-gate-and-coverage
KiSeungMin Sep 21, 2026
db69153
[Chore] 대시보드 평균 응답시간, 외부 API 성공률, Redis 히트율 패널의 값과 색을 고친다
KiSeungMin Sep 21, 2026
c66ffc4
Merge pull request #339 from AI-SIP/chore/dashboard-panel-values
KiSeungMin Sep 21, 2026
dc6abfe
[Fix] 관리자 로그인이 30분 만에 풀리던 문제를 고치고 유저 삭제를 없앤다
KiSeungMin Sep 21, 2026
4d5f991
[Feat] 관리자 화면 공통 레이아웃을 만들고 로그인, 공지, 피드백 화면을 새로 그린다
KiSeungMin Sep 21, 2026
f88ede0
[Feat] 관리자 화면에서 유저, 학습 기록, 스터디룸, 성장 데이터를 모두 볼 수 있게 한다
KiSeungMin Sep 21, 2026
2bfc193
[Fix] 관리자 화면의 박스 높이, 깜빡임, 로고를 고치고 사이드바를 접을 수 있게 한다
KiSeungMin Sep 21, 2026
1efc71b
[Feat] 관리자 통계와 요약 숫자에서 게스트와 관리자 계정을 뺀다
KiSeungMin Sep 21, 2026
abd05ae
[Chore] 관리자 화면에서 게스트 제외 안내 문구를 뺀다
KiSeungMin Sep 21, 2026
8c2dccf
[Fix] 관리자 화면의 오늘 날짜를 서울 시간대로 읽고 인증 테스트를 새 동작에 맞춘다
KiSeungMin Sep 21, 2026
191b7ca
Merge pull request #342 from AI-SIP/feat/admin-page-renewal
KiSeungMin Sep 21, 2026
c3eae5c
[Chore] 훈장 설명과 미션 제목의 한글 수를 숫자로 바꾼다
KiSeungMin Sep 21, 2026
979fb1b
Merge pull request #343 from AI-SIP/chore/achievement-mission-number-…
KiSeungMin Sep 21, 2026
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
10 changes: 5 additions & 5 deletions .github/workflows/ci-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,13 @@ jobs:
- name: Deploy Monitoring Stack
run: |
docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring pull \
mysql-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring stop prometheus-dev grafana-dev loki-dev promtail-dev mysql-exporter-dev || true
docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring rm -f prometheus-dev grafana-dev loki-dev promtail-dev mysql-exporter-dev || true
mysql-exporter-dev redis-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring stop prometheus-dev grafana-dev loki-dev promtail-dev mysql-exporter-dev redis-exporter-dev || true
docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring rm -f prometheus-dev grafana-dev loki-dev promtail-dev mysql-exporter-dev redis-exporter-dev || true
docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring up -d --force-recreate \
mysql-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
mysql-exporter-dev redis-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
docker-compose -f docker-compose.dev.yml --env-file .env.dev ps \
mysql-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
mysql-exporter-dev redis-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
shell: bash

- name: Verify Monitoring Integration
Expand Down
50 changes: 45 additions & 5 deletions .github/workflows/ci-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ jobs:
shell: bash

- name: Blue-Green Deployment
# 전환 전 확인(scripts/smoke/pre_switch_check.sh)이 스모크 계정 토큰을 만들 때 쓴다.
# 서버가 실제로 쓰는 서명 키는 APPLICATION_PROD 안의 jwt.*.secret 이다. 컨테이너에 넘기는 JWT_*_TOKEN_SECRET 환경변수는
# Spring 이 jwt.accessToken.secret 으로 읽지 않는다. access 키는 배포 뒤 스모크 full 이 통과해서 같은 값인 것이 확인됐고,
# refresh 키는 아직 확인되지 않아서 여기에는 넘기지 않는다. 배포 뒤 smoke-test.yml 이 refresh 까지 보고 알린다.
env:
SMOKE_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
SMOKE_USER_ID: ${{ secrets.SMOKE_PROD_USER_ID }}
run: |
export DOCKER_CONFIG=/tmp/.docker-ci
mkdir -p "$DOCKER_CONFIG"
Expand Down Expand Up @@ -158,6 +165,19 @@ jobs:
echo "Current active environment: $CURRENT (port $CURRENT_PORT)"
echo "Deploying to target environment: $TARGET (port $TARGET_PORT)"

# 배포를 끝내지 못하면 prod-latest 를 지금 사용자를 받는 이미지로 되돌린다. 위에서 이미 새 이미지로 옮겼고,
# 그대로 두면 누가 docker-compose up 을 다시 할 때 거절된 빌드가 올라간다.
PREV_IMAGE=""
if [ "$CURRENT" != "none" ]; then
PREV_IMAGE=$(docker inspect --format '{{.Image}}' "ono-app-prod-$CURRENT" 2>/dev/null || true)
fi
restore_latest_tag() {
if [ -n "$PREV_IMAGE" ]; then
$DOCKER tag "$PREV_IMAGE" ${{ secrets.DOCKER_USERNAME }}/ono:prod-latest \
|| echo "::warning::prod-latest 를 이전 이미지로 되돌리지 못했습니다"
fi
}

if [ "$TARGET" = "green" ]; then
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile green up -d app-prod-green
else
Expand All @@ -183,9 +203,29 @@ jobs:
if [ "$healthy" = false ]; then
echo "ERROR: $TARGET environment failed to become healthy within $timeout seconds"
docker-compose -f docker-compose.prod.yml logs --tail=100 app-prod-$TARGET
restore_latest_tag
exit 1
fi

# 전환 전 확인. 실패하면 nginx 를 건드리지 않았으니 이전 색이 계속 사용자를 받는다.
if ! bash ./scripts/smoke/pre_switch_check.sh "ono-app-prod-$TARGET" "$TARGET_PORT"; then
if [ "$CURRENT" = "none" ]; then
# 돌고 있는 앱이 없던 복구 배포다. 새 컨테이너를 내려도 나아지는 것이 없고 복구 수단만 막히니 경고만 하고 이어 간다.
echo "::warning::전환 전 확인이 실패했지만 돌고 있던 앱이 없어 그대로 전환합니다. 스모크 결과를 확인해야 합니다"
else
echo "ERROR: $TARGET environment failed the pre-switch check. Nginx stays on $CURRENT"
docker-compose -f docker-compose.prod.yml logs --tail=100 app-prod-$TARGET
# 새 컨테이너는 이름으로 지운다. 남아 있으면 다음 배포가 blue 를 먼저 보고 현재 색을 잘못 읽어,
# 사용자를 받고 있는 색을 다시 만들 수 있다.
docker rm -f "ono-app-prod-$TARGET" || true
if [ -n "$(docker ps -aq --filter "name=^ono-app-prod-$TARGET\$")" ]; then
echo "::error::ono-app-prod-$TARGET 를 지우지 못했습니다. 다음 배포 전에 직접 지워야 합니다. 남아 있으면 다음 배포가 현재 색을 잘못 판단합니다"
fi
restore_latest_tag
exit 1
fi
fi

echo "Switching Nginx to $TARGET environment (port $TARGET_PORT)..."
sudo /opt/ono/scripts/switch-nginx.sh $TARGET_PORT

Expand Down Expand Up @@ -219,13 +259,13 @@ jobs:
- name: Deploy Monitoring Stack
run: |
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring pull \
mysql-exporter-prod prometheus-prod loki-prod promtail-prod grafana-prod
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring stop prometheus-prod grafana-prod loki-prod promtail-prod mysql-exporter-prod || true
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring rm -f prometheus-prod grafana-prod loki-prod promtail-prod mysql-exporter-prod || true
mysql-exporter-prod redis-exporter-prod alertmanager-prod prometheus-prod loki-prod promtail-prod grafana-prod
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring stop prometheus-prod grafana-prod loki-prod promtail-prod mysql-exporter-prod redis-exporter-prod alertmanager-prod || true
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring rm -f prometheus-prod grafana-prod loki-prod promtail-prod mysql-exporter-prod redis-exporter-prod alertmanager-prod || true
docker-compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring up -d --force-recreate \
mysql-exporter-prod prometheus-prod loki-prod promtail-prod grafana-prod
mysql-exporter-prod redis-exporter-prod alertmanager-prod prometheus-prod loki-prod promtail-prod grafana-prod
docker-compose -f docker-compose.prod.yml --env-file .env.prod ps \
mysql-exporter-prod prometheus-prod loki-prod promtail-prod grafana-prod
mysql-exporter-prod redis-exporter-prod alertmanager-prod prometheus-prod loki-prod promtail-prod grafana-prod
shell: bash

- name: Verify Monitoring Integration
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/restart-monitoring.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
RABBITMQ_PASSWORD=${{ secrets.RABBITMQ_PASSWORD }}
GRAFANA_ADMIN_USER=${{ secrets.GRAFANA_ADMIN_USER }}
GRAFANA_ADMIN_PASSWORD=${{ secrets.GRAFANA_ADMIN_PASSWORD }}
DISCORD_WEBHOOK_URL=${{ secrets.DISCORD_WEBHOOK_URL }}
GRAFANA_PROD_ROOT_URL=https://ono-prod.seungminki.shop/grafana/
GRAFANA_PROD_SERVE_FROM_SUB_PATH=true
PROMETHEUS_PROD_EXTERNAL_URL=https://ono-prod.seungminki.shop/prometheus/
Expand All @@ -56,16 +57,16 @@ jobs:
if: ${{ github.event.inputs.target == 'dev' }}
run: |
docker compose -f docker-compose.dev.yml --env-file .env.dev --profile monitoring up -d --force-recreate \
mysql-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
mysql-exporter-dev redis-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
docker compose -f docker-compose.dev.yml --env-file .env.dev ps \
mysql-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
mysql-exporter-dev redis-exporter-dev prometheus-dev loki-dev promtail-dev grafana-dev
shell: bash

- name: Restart monitoring stack (prod)
if: ${{ github.event.inputs.target == 'prod' }}
run: |
docker compose -f docker-compose.prod.yml --env-file .env.prod --profile monitoring up -d --force-recreate \
mysql-exporter-prod prometheus-prod loki-prod promtail-prod grafana-prod
mysql-exporter-prod redis-exporter-prod alertmanager-prod prometheus-prod loki-prod promtail-prod grafana-prod
docker compose -f docker-compose.prod.yml --env-file .env.prod ps \
mysql-exporter-prod prometheus-prod loki-prod promtail-prod grafana-prod
mysql-exporter-prod redis-exporter-prod alertmanager-prod prometheus-prod loki-prod promtail-prod grafana-prod
shell: bash
14 changes: 9 additions & 5 deletions .github/workflows/smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ name: Smoke Test
# - 보낼 수 있는 요청과 요청 수 상한은 스크립트에 고정되어 있다.
#
# 필요한 Secrets (없으면 할 수 있는 단계까지만 한다)
# JWT_ACCESS_TOKEN_SECRET, SMOKE_PROD_USER_ID, SMOKE_DEV_USER_ID, DISCORD_WEBHOOK_URL
# JWT_ACCESS_TOKEN_SECRET, JWT_REFRESH_TOKEN_SECRET, SMOKE_PROD_USER_ID, SMOKE_DEV_USER_ID, DISCORD_WEBHOOK_URL
#
# JWT_ACCESS_TOKEN_SECRET 은 배포 워크플로가 서버에 넘기는 그 키다(ci-dev.yml, ci-prod.yml). 스모크는 같은 키로
# 토큰을 직접 만들어 쓰기 때문에 값이 같아야 하고, 따로 복사해 두면 키를 바꿀 때 한쪽만 남아 알림이 계속 울린다.
# JWT_*_TOKEN_SECRET 은 배포 워크플로가 컨테이너에 환경변수로 넘기는 값이지만, 서버가 실제로 쓰는 서명 키는
# APPLICATION_PROD 안의 jwt.accessToken.secret, jwt.refreshToken.secret 이다(환경변수 이름이 Spring 설정 키와 이어지지 않는다).
# 스모크는 이 시크릿으로 토큰을 직접 만들기 때문에 두 곳의 값이 같아야 한다. 서버 키를 바꾸면 이 시크릿도 같이 바꾼다.
# refresh 키로는 서버에 저장된 적 없는 토큰을 만들어 갱신 경로가 1002 로 답하는지만 본다(서버에 쓰기 없음).
#
# workflow_run 과 schedule 은 이 파일이 기본 브랜치(main)에 있어야 동작한다.

Expand Down Expand Up @@ -128,10 +130,11 @@ jobs:
SMOKE_BASE_URL: ${{ steps.plan.outputs.url }}
SMOKE_LEVEL: ${{ steps.plan.outputs.level }}
SMOKE_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
SMOKE_REFRESH_TOKEN_SECRET: ${{ secrets.JWT_REFRESH_TOKEN_SECRET }}
SMOKE_USER_ID: ${{ secrets.SMOKE_PROD_USER_ID }}
run: |
if [ "$SMOKE_LEVEL" = "bootstrap" ]; then
unset SMOKE_ACCESS_TOKEN_SECRET
unset SMOKE_ACCESS_TOKEN_SECRET SMOKE_REFRESH_TOKEN_SECRET
python3 scripts/smoke/smoke_test.py bootstrap
else
python3 scripts/smoke/smoke_test.py
Expand All @@ -144,10 +147,11 @@ jobs:
SMOKE_BASE_URL: ${{ steps.plan.outputs.url }}
SMOKE_LEVEL: ${{ steps.plan.outputs.level }}
SMOKE_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
SMOKE_REFRESH_TOKEN_SECRET: ${{ secrets.JWT_REFRESH_TOKEN_SECRET }}
SMOKE_USER_ID: ${{ secrets.SMOKE_DEV_USER_ID }}
run: |
if [ "$SMOKE_LEVEL" = "bootstrap" ]; then
unset SMOKE_ACCESS_TOKEN_SECRET
unset SMOKE_ACCESS_TOKEN_SECRET SMOKE_REFRESH_TOKEN_SECRET
python3 scripts/smoke/smoke_test.py bootstrap
else
python3 scripts/smoke/smoke_test.py
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Test

# main / develop 으로 합치기 전에 반드시 통과해야 하는 게이트.
# 기존 ci-dev / ci-prod 는 workflow_dispatch 수동 실행이고 `-x test` 로 테스트를 건너뛴다.
# 배포 파이프라인은 그대로 두고, 검증만 이 워크플로가 책임진다.
# ci-dev 는 workflow_dispatch 수동 실행이고, ci-prod 는 main push 로 바로 배포를 시작한다. 둘 다 `-x test` 로
# 테스트를 건너뛰고 이 워크플로를 기다리지 않는다. 그래서 main push 때 도는 이 워크플로는 배포와 나란히 돌 뿐이고,
# 배포를 막는 것은 PR 단계다. main 과 develop 의 branch protection 이 이 job(`격리 환경 테스트`)을 required check 로 건다.
on:
pull_request:
branches: [main, develop]
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,19 @@ services:
profiles:
- monitoring

redis-exporter-dev:
image: oliver006/redis_exporter:v1.67.0
container_name: ono-redis-exporter-dev
restart: unless-stopped
environment:
REDIS_ADDR: redis://redis-dev:6379
depends_on:
- redis-dev
networks:
- ono-network
profiles:
- monitoring

# Grafana - DEVELOPMENT (Profile: monitoring)
grafana-dev:
image: grafana/grafana:latest
Expand Down
37 changes: 37 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,41 @@ services:
profiles:
- monitoring

redis-exporter-prod:
image: oliver006/redis_exporter:v1.67.0
container_name: ono-redis-exporter-prod
restart: unless-stopped
environment:
REDIS_ADDR: redis://redis-prod:6379
depends_on:
- redis-prod
networks:
- ono-network
profiles:
- monitoring

# Alertmanager - PRODUCTION (Profile: monitoring)
# Prometheus 알림을 디스코드로 보낸다. 웹훅 주소를 설정 파일에 적지 않으려고
# 뜰 때 환경변수를 파일로 옮기고 alertmanager.prod.yml 의 webhook_url_file 로 읽는다.
alertmanager-prod:
image: prom/alertmanager:v0.28.1
container_name: ono-alertmanager-prod
restart: unless-stopped
environment:
DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL}
entrypoint: ["/bin/sh", "-c"]
command:
- |
printf '%s' "$$DISCORD_WEBHOOK_URL" > /tmp/discord_webhook_url
exec /bin/alertmanager --config.file=/etc/alertmanager/alertmanager.yml --storage.path=/alertmanager --cluster.listen-address=
volumes:
- ./monitoring/alertmanager.prod.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager_prod_data:/alertmanager
networks:
- ono-network
profiles:
- monitoring

# Grafana - PRODUCTION (Profile: monitoring)
grafana-prod:
image: grafana/grafana:latest
Expand Down Expand Up @@ -320,3 +355,5 @@ volumes:
driver: local
grafana_prod_data:
driver: local
alertmanager_prod_data:
driver: local
19 changes: 19 additions & 0 deletions monitoring/alertmanager.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Prometheus 가 평가한 알림(monitoring/alerts/ono-alerts.yml)을 디스코드로 보낸다.
# 웹훅 주소는 앱 에러 알림과 같은 DISCORD_WEBHOOK_URL 을 쓰고, 컨테이너가 뜰 때
# 환경변수를 파일로 옮겨 둔다(docker-compose.prod.yml 의 alertmanager-prod 참고).
# 설정 파일에 비밀값을 적지 않기 위해 webhook_url 대신 webhook_url_file 을 쓴다.

route:
receiver: discord
group_by: [alertname, job]
# 같은 알림이 여러 개 동시에 뜨면 30초 모아서 한 번에 보낸다.
group_wait: 30s
group_interval: 5m
# 해결되지 않은 알림은 4시간마다 다시 알린다.
repeat_interval: 4h

receivers:
- name: discord
discord_configs:
- webhook_url_file: /tmp/discord_webhook_url
send_resolved: true
Loading
Loading