Conversation
Packer 기반 artifact release pipeline과 dev 배포 구조 추가
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 939e481465
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| instance_refresh { | ||
| strategy = "Rolling" |
There was a problem hiding this comment.
Persist uploads before triggering instance refreshes
When a production release changes the launch template or ArtifactRelease tag, this rolling refresh terminates the existing instances. However, platform/docker-compose.production.yml bind-mounts uploads from .data/CTFd/uploads, and the launch template stores that directory on a root EBS volume configured with delete_on_termination = true; consequently, every artifact deployment can permanently delete uploaded challenge/user files. Move uploads to shared durable storage or preserve/migrate them before enabling automatic refreshes.
Useful? React with 👍 / 👎.
| resource "aws_ecr_repository" "ctfd" { | ||
| name = local.ctfd_repository_name | ||
| image_tag_mutability = "IMMUTABLE" | ||
| force_delete = false |
There was a problem hiding this comment.
Migrate existing ECR state before creating foundation repositories
For an environment deployed from the parent configuration, the removed module.ami already owns repositories named ${var.prefix}-ctfd and ${var.prefix}-sql-judge; with the default values, these are the exact names this new foundation state attempts to create. Following the documented foundation-first upgrade therefore fails with RepositoryAlreadyExists, while applying the old runtime state removal first schedules the force-deletable repositories and their images for destruction. Add an explicit cross-state move/import migration or use non-conflicting repository names so existing deployments can adopt the pipeline safely.
Useful? React with 👍 / 👎.
아티팩트 빌드 시간을 단축한다
Independent review against MySQL 8.4.11 and the built judge container reproduced three student-reachable problems, all fixed here: - Event scheduler threads outlived DB and account cleanup (2 of 40 requests) and the sweeper could not stop them: start MySQL with --event-scheduler=DISABLED and make the sweeper kill ct_% sessions by name pattern instead of by mysql.user membership. - The MAX_EXECUTION_TIME optimizer hint overrode the session limit: reject the hint in validateSQLQuery. - DML is not bounded by max_execution_time and its rollback doubled the cost of a killed statement: run the graded statement under a separate SELECT-only account while a full-privilege init account loads the challenge SQL. The graded connection is pinned so the session limit applies to that statement. Also align collation with the MySQL 8 default (utf8mb4_0900_ai_ci) so ordering matches a student's local MySQL, give sql-judge a /health healthcheck, and make CTFd wait for it so a fresh instance never accepts submissions it cannot grade.
scripts/regrade-challenges sends every challenge definition to a judge with the solution as both solution and submission, records status, latency, row count and result size, and optionally compares result rows against a baseline judge (for example the previous go-mysql-server build). Numeric formatting differences are classified separately from value differences. The input is a JSON array of challenge definitions only; no user or submission data is involved.
SQL Judge 실행 엔진을 실제 MySQL 8.4로 교체
Re-grading last semester's 61 challenges on the MySQL judge failed 58 of them at init: the scripts are MySQL Workbench and mysqldump exports that manage their own schema and session. - Skip CREATE/DROP DATABASE|SCHEMA and USE statements and map that schema name onto the execution's temporary database, including mysqldump's /*!... */ wrapped forms and schema-qualified names such as kbo.PLAYER in init and graded statements. - Drop leading comment lines from each statement; MySQL rejects "-----" separators and empty comment-only chunks. - Carry the init session's sql_mode (for example TRADITIONAL) into the graded statement's session, as one local MySQL session would. - Start MySQL with lower_case_table_names=1 so Salaries and salaries match, as on Windows and macOS where the challenges were written. The re-grade tool now treats float-precision differences as formatting.
judge가 로컬 MySQL 템플릿 형식의 init SQL을 받아들이도록 수정
AUTHORING.md explains how the judge executes init and graded SQL, what a local MySQL export may contain, how results are compared (unique ORDER BY, numeric text formats), and which words the filter blocks. The admin create and update forms link to it, as does the plugin README.
SQL 문제 출제 가이드 추가
scripts/export-challenges pulls SQL challenge definitions from a CTFd through the admin API (token from the environment), and scripts/review-challenges starts a disposable MySQL judge from the checkout, grades every definition with scripts/regrade-challenges --review, prints authoring findings, and removes the containers. The review findings cover execution failures, missing ORDER BY, repeated sort-key values, ORDER BY expressions the tool cannot check, unrounded AVG/division results, empty or near-limit results, and slow requests. REVIEW.md documents the procedure and how to act on each finding so a future TA or an agent can run it unchanged.
문제 검토 파이프라인 추가 (export, review, 판정)
The plugin's read() always included init_query and solution_query, and CTFd returns that dict from GET /api/v1/challenges/<id> to every logged-in user, so any student could fetch the answer key. Only admins receive the two fields now; the student view never used them. A plugin test in the CTFd harness locks this in.
학생에게 SQL 문제 정답이 노출되던 API 응답 수정
The page's tracker flushes every 5 seconds or 20 events and puts a rejected batch back in front of its buffer to retry forever. After one transient failure the buffer passes 50 events, and a count-based 400 then leaves the page stuck, silently losing every later event. Only the body size is bounded now; the 50-event figure remains as the sizing basis for that cap.
…ity on sign-out already_solved on client events is decided at the event's own timestamp (solves dated before it), so work buffered just before the first correct submission is not marked as after it; execute events use the same rule at the moment of the run. When a session is signed out because an API token replaced the active nonce, its activity marker is removed too, so the next form login is not logged as concurrent.
Test 실행을 execute 이벤트로 서버에서 기록하고 동시 접속을 로그에 남김 (DDPS-962, DDPS-1303)
…e exam browser Since only one session per account is ever alive, the sequence of logins is the whole record of who held an account and when. Each login now writes one line with the browser and the previous login of the account (time, address, browser). The activity marker, the "attempt while another session is active" line and the "signed out" line from #68 are removed; they tried to decide concurrency on the server and needed state that the login trail makes unnecessary. While the exam-browser restriction is on, a student's login from any other browser is refused before it happens. The login page stayed open for admins, but a successful login replaces the account's session and would have signed the student out of the exam browser.
The login line is written only when the pre-request nonce snapshot exists; a request refused by an earlier hook has none and is not a login. During the exam-browser restriction every non-admin name, existing or not, gets the same 403 from another browser, and refused attempts count against the login view's rate limit.
…address A configured preset admin exists only after its first login, so its name or email is recognised before the database lookup. The previous login record stores get_ip(), the same resolved address CTFd's log lines use behind a proxy.
Two TAs may share an admin account during an exam, and an admin's token script runs beside the browser. Both the plugin hook and the fork's check in get_current_user() now skip admin accounts; students are still limited to one session.
…xam-start logins The one-session rule for students is now a switch on the Exam Mode page, off by default so students may move between devices outside exams; the login trail is written either way. While any exam rule is on, every admin page shows a banner so it is not forgotten. The login page hides the Google button while the exam-browser rule is on, and the form login limit rises to 120 per 5 seconds per address for a lecture hall behind one NAT. The login trail line now reads "session started" so it is not confused with CTFd's own "logged in" line.
…, log registrations The admin banner was inserted as the first child of <body>, where the admin theme's fixed navbar covers it. It now goes into <main>, which starts below the navbar, the same place the core update notice uses. The exam-time login check let any name that matched the preset admin name or email through. It now looks the account up first: an existing account is judged by its type, and only a preset admin that does not exist yet is recognised, by the same name and password auth.login accepts. Registration also starts a session, so auth.register is in the login endpoint map and writes a "session started via registration" line.
…urse domain a setting The first lecture is where a whole class signs in with Google in the same minute from one lecture-hall NAT address, so the per-IP callback limit goes from 60 to 300 per minute. Google has already authenticated the request by then, so the limit only guards against abuse. The course domain was a constant in auth.py. It is now GOOGLE_HOSTED_DOMAIN in config.ini or the environment, hanyang.ac.kr when unset, and the userdata passes it through from the application secret when present.
… lookup The refusal hook looked the submitted account up before checking the shared login rate limit, so an address already over the limit still cost one query per attempt. The limit is now checked first, and only a refused attempt below it is counted.
로그인 기록을 한 줄로 단순화하고 시험 세션 규칙(단일 세션·브라우저 제한)을 스위치와 배너로 관리
CTFd builds attachment download links as S3 presigned URLs. With boto3's default addressing the URL points at the global host (bucket.s3.amazonaws.com), which S3 answers with a redirect to the regional host for a bucket outside us-east-1. The redirected request no longer matches the signature, so every download ended in 403 on dev. Set AWS_S3_ADDRESSING_STYLE to virtual so the URL is signed for the regional host, add a test that pins the setting, and note the reason in the IaC README.
S3 첨부 파일 다운로드 링크를 리전 호스트로 서명 (다운로드 403 수정)
온보딩 페이지에서 비밀번호 규칙과 약관 체크를 입력 중에 바로 표시
…footer off the form Both forms placed the submit button in a right-aligned third of the row, leaving an empty two-thirds to its left. The button now spans the form like the Google button below it. The form column also gets a bottom margin so the footer no longer sits directly under the button on the longer onboarding page.
API tokens are for admin scripts; a student has no use for the tab, so the settings page renders the tab and its pane only for admins.
Each failure came from a deliberate change of this fork that the upstream test never learned about: - the default theme is ddps, so the prefix-loading test compares against the default theme's prefix instead of core's; - oauth_id is a string column (Google accounts store "google_<id>"), so the MLC path now stores the ids as strings on every database and the test expects strings; - the ddps settings page has no website or country field, so the test checks those two through the API and the rendered fields on the page; - the ddps navbar names the logged-in user, so the hidden-user test searches the page content instead of the whole body.
Hiding the settings tab alone left the token API open to students, and a student who already held a token could no longer see or revoke it. The onboarding plugin's account-policy hook now refuses a student's POST /api/v1/tokens and any request a student signs in with a token, so no student token can exist or work.
…opened CTFd's tokens hook signs the request in before the plugin hook runs, so refusing with 403 alone left a session cookie a client could reuse without the token. The hook now logs that session out before refusing.
The token refusal sat behind request_is_exempt(), so a student token sent to an exempt endpoint such as /healthcheck still left the session CTFd's tokens hook had opened. The check now runs first in the hook.
Refusing after CTFd's tokens hook still let login_user() replace the account's active nonce, which signs the student's browser out under the one-session rule. The onboarding plugin now puts its check at the front of the before_request list and answers 403 before any login; unknown or expired tokens are left to CTFd's hook.
로그인·온보딩 화면의 제출 버튼을 폼 너비로 늘리고 설정 화면의 토큰 탭을 관리자에게만 보임
오래 실패하던 회귀 테스트 4건을 포크의 의도된 동작에 맞게 정리
SQL 채점 공정성과 시험 운영 안정성 개선
개요
dev에 누적된 커밋 105개(PR 22개)를main에 반영합니다. 처음 열었을 때는 artifact 기반 배포 파이프라인만 담고 있었지만, 이후 SQL Judge 실행 엔진 교체, 문제 출제 가이드와 검토 파이프라인, 학생에게 정답 SQL이 노출되던 API 수정, 시험에 대비한 ASG 설정, 업로드 파일의 S3 저장, 로그인 방식 전환(Google은 첫 접속의 수강생 확인에만 쓰고 이후 아이디·비밀번호), 시험 무결성 로그(Test 실행 기록과 동시 접속 추적), 첨부 파일 다운로드 링크 수정까지 포함하게 되었습니다. 각 PR에 자동으로 달린 Codex 리뷰 지적은 모두 후속 PR(#58~#61)로 반영했습니다. 병합 시점에 운영 release는 이 PR의 마지막 커밋(#56 포함) 기준으로 다시 빌드해야 합니다.변경 내용
아티팩트 기반 배포 파이프라인 (#49, #51)
IaC/ARTIFACT_PIPELINE.md에 정리했습니다.SQL Judge 실행 엔진을 MySQL 8.4로 교체 (#52, #53, #59)
go-mysql-server를 실제 MySQL 8.4.11(digest 고정,judge-db내부 네트워크에만 연결)로 교체했습니다. 수업 SQL이 학생 로컬 MySQL 8과 같은 의미로 실행됩니다.ctfd_tmp_<hex>데이터베이스와 계정 둘을 만듭니다. init 계정은 해당 DB에만ALL PRIVILEGES, 채점 계정은SELECT, SHOW VIEW만 가집니다. 정답과 제출은 서로 다른 DB·계정에서 실행하고 끝나면 세션 KILL 후 삭제합니다. 정리 실패분은 5분 주기 스위퍼가 회수합니다.MAX_EXECUTION_TIME힌트 차단, 이벤트 스케줄러DISABLED..env.judge(0600)에 한 번 생성하고 이후 부팅에서 보존합니다. CTFd 컨테이너는 이 파일과judge-db네트워크에 접근하지 않습니다./healthhealthcheck를 기다린 뒤 기동합니다.utf8mb4_0900_ai_ci를 따릅니다.DROP/CREATE SCHEMA kbo; USE kbo;,SET SQL_MODE='TRADITIONAL')을 받아들입니다. 스키마 문장은 실행하지 않고 그 이름을 임시 DB 별칭으로 치환하며, init 세션의sql_mode를 채점 세션에 그대로 적용합니다.'[email protected]'보존),USE `kbo-data`같은 백틱 스키마 이름도 인식합니다.사용자·관리자가 체감하는 계약 변화는 PR #52 본문의 "계약 변화" 절에 정리되어 있습니다. 요약하면 실행 시간 예산이 실행당 5초에서 요청 8초·실행당 2.5초로 바뀌고, 결과 텍스트가 실제 MySQL 표현(
5.0000,12.5000,2024-03-23)을 따르며, 채점 문장은 읽기 전용 계정으로 실행됩니다.문제 출제 가이드와 검토 파이프라인 (#54, #55, #60)
platform/CTFd/plugins/sql_challenges/AUTHORING.md: 실행 모델과 한도, init에서 허용·비허용·권장하는 문장, 정답 SQL 규칙(ORDER BY와 tie-break, ROUND), 차단 단어, 게시 전 확인 항목. 관리자 문제 생성·수정 화면에서 링크합니다.REVIEW.md와scripts/export-challenges→scripts/review-challenges --review: 관리자 토큰으로 문제 정의를 내보내고, 일회용 MySQL 8.4 + 이 checkout의 judge로 전 문제를 채점한 뒤 문제별 판정(실행 실패, ORDER BY 없음, 정렬 키 동률, ROUND 없는 집계, 0행·500행 이상, 느린 요청)을 출력합니다. 다음 학기 조교가 문서만 보고 그대로 실행할 수 있게 만들었습니다.scripts/regrade-challenges: 두 judge에 같은 입력을 보내 결과를 비교하는 재채점 도구. judge 구조를 바꿀 때 회귀 검증에 씁니다. 숫자 비교는 #60에서 표시 정밀도 기준으로 바꿔,183.0901↔183.0900900900901은 표현 차이로 두되1000000↔1000001은 값 차이로 잡습니다.학생에게 정답 SQL이 노출되던 API 수정 (#56)
read()가init_query와solution_query를 항상 응답에 넣어, 로그인한 학생이GET /api/v1/challenges/<id>로 정답 SQL을 볼 수 있었습니다. 플러그인 초기부터 있던 결함이며 이번 judge 작업과는 무관합니다.is_admin()일 때만 넣고deadline은 모든 사용자에게 유지합니다. CTFd 테스트 하네스로 관리자·학생 응답을 고정했습니다.main-939e48146560-20260901T104354Z에는 포함되어 있지 않습니다.시험에 대비한 ASG 설정 (#57, #58)
health_check_grace_period와 instance refreshinstance_warmup을 180초에서 300초로 올렸습니다. CTFd가 MySQL 초기화와 judge healthcheck를 기다리게 되면서 spot t4g.small의 launch→ALB healthy가 3분 50초로 측정됐기 때문입니다.exam_windows변수(KST,{ name, start, end, capacity })로 시험·퀴즈 창을 선언하면 창마다aws_autoscaling_schedule두 개를 만듭니다. 시작 시각에min_size·desired_capacity를capacity로 올리고, 종료 시각에min_size만 되돌려 target tracking이 서서히 줄이게 합니다. 지난 창은 plan에서 제외됩니다. 지난 학기부터 손으로 하던 시험 전 8대 스케일 관행을 IaC로 옮긴 것입니다.max_size를 건드리지 않고(-1) capacity는asg_min_size~asg_max_size범위로 검증하며, 겹치거나 맞닿는 창은 validation이 거부합니다. 연달아 보는 시험은 한 창으로 합칩니다.on_demand_percentage_above_base0→100). 시험 중 spot 회수를 피하기 위한 것이며 t4g.small 기준 비용 차이는 학기당 몇 달러입니다. spot으로 되돌리려면 tfvars에서 0으로 둡니다.desired_capacity·min_size를ignore_changes로 두어 예약 작업이나 스케일링이 바꾼 값을 시험 중 apply가 줄이지 않게 했습니다.IaC/README.md에 Auto Scaling 설정과 시험·퀴즈 사전 스케일 절을 추가했고tests/test_asg_exam_readiness.py로 값을 고정했습니다.업로드 파일의 S3 저장과 ECR 이관 문서 (#61)
UPLOAD_PROVIDER=s3(인스턴스 role, bucket 한정 권한)로 고쳤습니다. ephemeral(dev) 배포만 bucket을force_destroy합니다.RepositoryAlreadyExists로 실패하는 경우의 이관 절차를IaC/ARTIFACT_PIPELINE.md에 적었습니다.sql-2026-s2는 새 prefix라 해당되지 않습니다.로그인 방식 전환: Google 관문 + 온보딩 + 폼 로그인 (#62)
/google/callback의 IP당 60초 10회 rate limit이며, 강의실·교내 NAT 뒤에서는 수십 명이 같은 IP로 보입니다. 이제 Google 로그인은 첫 접속에서 한양 계정을 확인하고 계정을 만드는 용도로만 씁니다./onboarding/(새 플러그인CTFd/plugins/onboarding, blueprint +before_request훅)으로 가서 아이디(자유 선택)·비밀번호·학번을 정하고, 이후에는 CTFd 기본 로그인 폼(아이디 또는 이메일 + 비밀번호)을 씁니다. 비밀번호를 잊으면 Google로 다시 로그인하며, 그 세션에서는 온보딩 페이지가 한 번 "새 비밀번호 설정" 모드로 나옵니다.verified_email,hd == hanyang.ac.kr, 이메일 접미사를 모두 검사합니다. 이전에는hd가 인가 URL의 힌트일 뿐이라 다른 Google 계정도 가입됐습니다./loginPOST 5초 30회,/google/callback60초 60회로 올렸습니다. 로그인 화면은 폼이 먼저이고 Google 버튼은 보조 경로입니다. Codex 지적 4건을 반영했습니다.온보딩 약관 동의, 시험 브라우저 제한, 푸터 연도 (#64)
tos_text, 플러그인 초안으로 seed) 동의를 함께 받고, 동의를 필수·비편집 boolean 사용자 필드로 기록합니다. 동의 기록이 없는 계정은 약관 전용 화면을 한 번 거칩니다. Google 재로그인은 반드시 새 비밀번호를 정해야 하며, 비밀번호는 8자 이상에 문자·숫자를 포함해야 합니다. 학생은 이메일을 바꿀 수 없습니다.Trustlockbrowser가 없는 요청은 403이며, 로그인·로그아웃·비밀번호 복구·정적 파일·healthcheck는 예외입니다. 문제·해설 첨부 파일은 제한 대상입니다.동시 로그인 차단을 모든 요청에서 검사 (#66)
get_current_user()안에만 있어 순위표처럼 사용자 전체를 읽지 않는 페이지에서는 이전 세션이 통과했습니다. 플러그인single_session의before_request훅이 로그인된 모든 요청에서 세션 nonce와 활성 nonce를 비교해 다르면 로그아웃시키고 로그인 페이지로 보냅니다(API는 401). API 토큰 요청은 검사하지 않습니다. 코어 수정은 없습니다.ALB 상태 검사를
/healthcheck로 변경 (#67)/가 403을 받아 ASG가 6분마다 인스턴스를 교체했습니다(12:24~12:52Z, 교체 5회). 대상 그룹 상태 검사를 CTFd/healthcheck(DB·설정 정상일 때만 200, 두 플러그인 모두 예외)로 바꿔 스위치를 켜도 인스턴스가 내려가지 않게 했습니다. 운영 반영 전 필수입니다.Test 실행의 execute 이벤트와 동시 접속 기록 (#68, DDPS-962·DDPS-1303)
execute이벤트를 행동 로그 파일에 서버 기준(사용자·문제·판정,source: "server")으로 씁니다. 행동 로그 엔드포인트는 이벤트 종류 허용 목록, 존재하는 문제, 텍스트 필드 8,000자, 본문 크기 상한을 검사하고 사용자·문제 필드를 서버 값으로 덮어씁니다. 요청당 이벤트 건수는 제한하지 않습니다. 페이지 tracker가 거부된 배치를 무한 재시도하므로 건수 제한은 일시 장애 뒤 페이지를 영구히 막습니다(검토 중b0659ff9로 보완).single_session플러그인이logins스트림에 "다른 세션이 살아 있는 상태의 로그인 시도"와 "다른 브라우저 로그인으로 끊김" 두 줄을 남깁니다. 최근 30분 활동 기준으로 로그아웃 뒤 재로그인과 구분합니다. (#69에서session started한 줄로 대체)로그인 기록 한 줄화와 시험 세션 규칙 스위치 (#69)
logins스트림에 브라우저와 직전 로그인(시각·IP·브라우저)을 담은session started줄 하나를 남기고, #68의 활동 키·경고 줄은 제거했습니다.get_current_user()의 nonce 검사도 같은 조건). 기본은 꺼짐이라 평소에는 기기를 오갈 수 있습니다.GOOGLE_HOSTED_DOMAIN설정(config.ini 또는 환경변수, 기본 hanyang.ac.kr)입니다.S3 첨부 파일 다운로드 링크를 리전 호스트로 서명 (#70)
<bucket>.s3.amazonaws.com)로 서명되는데, ap-northeast-2 버킷은 S3가 리전 호스트로 리다이렉트해 서명이 어긋납니다(SignatureDoesNotMatch).IaC/ec2/userdata.sh의 CTFd 환경에AWS_S3_ADDRESSING_STYLE=virtual을 넣어 리전 호스트로 서명하게 했습니다. 코드 변경은 없고, 설정을 고정하는 테스트와 README 설명을 추가했습니다. 운영 uploads 버킷도 새로 만들어지므로 이 수정 없이는 운영에서도 같은 403이 납니다.온보딩 페이지에서 비밀번호 규칙과 약관 체크를 입력 중에 표시 (#65)
로그인·온보딩 화면의 버튼 너비와 설정 화면의 토큰 탭 (#71)
POST /api/v1/tokens를 403으로 거부하고 학생 토큰으로 인증하는 요청도 403으로 거부합니다. 이 거부는 CTFd의 tokens 훅보다 먼저 실행되어login_user()자체가 일어나지 않으므로, 단일 세션 규칙 아래에서 옛 토큰이 학생의 시험 브라우저 세션을 끊지 못합니다. 관리자 토큰은 그대로 동작합니다. Codex P1 3건·P2 1건을 반영했습니다.오래 실패하던 회귀 테스트 4건 정리 (#72)
ddps,oauth_id문자열 컬럼, ddps 설정 화면의 필드 구성, 내비바의 사용자 이름 표시)을 반영하지 못한 경우였습니다. 테스트를 포크의 동작에 맞추고, MLC 경로(auth.py)가 id를 문자열로 저장하도록 명시했습니다. 코드 결함은 없었습니다.검증 기록
2026-09-01 main·dev 동시 운영 (아티팩트 파이프라인)
sql-2026-s2-dev,sql-dev.ddps.cloud,10.26.0.0/16sql-2026-s2,sql.ddps.cloud,192.168.0.0/16main-939e48146560-20260901T104354Z, AMIami-002ad5ac61eb67837sql.ddps.cloud:/setup으로 정상 연결,sql-dev.ddps.cloud: HTTP 2002026-09-02 dev 배포 (MySQL judge)
dev@65f890ca를 releasedev-65f890cae7b0-20260902T050048Z(AMIami-0bbb1474ddbc824b2)로 빌드해sql-dev.ddps.cloud(t4g.small)에 instance refresh로 반영했습니다. 부팅 후 MySQL 초기화 약 25초, CTFd 기동까지 약 2분 15초, ALB healthy까지 3~3분 50초였습니다..env.judge0600, CTFd에서 MySQL 비접근,event_scheduler=DISABLED,utf8mb4_0900_ai_ci, OOM 0을 확인했습니다.지난 학기 문제 61개 전수 재채점(같은 t4g.small, 옛 judge는
dev@72b1775d):실패한 1건(
#32)은 정답 SQL의 ambiguousGROUP BY TEAM_ID로 로컬 MySQL에서도 같은 오류이며 문제 수정 대상입니다. 옛 엔진과의 결과 차이는 숫자 표현 17건, 행 순서 6건(ORDER BY 없음 또는 tie-break 없음:#22·#23·#27·#30·#36·#57), DATE 표현 2건으로, 정답과 제출이 같은 엔진에서 실행되므로 채점에는 영향이 없습니다.부하 실측(5테이블×100행 init): 동시 16에서 7.2 req/s·p95 2.5초, 동시 32에서는 96건 중 26건이 busy 응답. 5테이블×1000행 init은 동시 16에서 3.9 req/s·p95 4.7초. 인스턴스당 채점 상한은 초당 5~8건입니다.
마무리 검증: 2대 scale-out(spot), 세션 유지 40/40, 두 judge에 걸친 채점 20/20, 인스턴스 재기동 후
.env.judge보존, CloudWatchsql-judge스트림 수집, CTFd 웹 경로(setup → 관리자 로그인 → SQL 문제 생성 → Test → Submit) 정상. 검증 후 dev runtime 45개 자원과 dev secret은 삭제했고 release·AMI는 유지했습니다.2026-09-02 ASG·업로드 설정 plan (#57, #58, #61)
빈 dev state에 샘플 시험 창 2개(하나는 지난 날짜)를 넣고 plan만 실행했습니다. 47 to add, 지난 창은 제외,
midterm은 시작2026-10-19T23:30:00Z·종료2026-10-20T02:30:00Z(KST 08:30/11:30)로 min·desired 8, 종료 시 min 1·desired 유지. grace 300, warmup 300, 온디맨드 100 확인. #58 이후에는 창 2개(midterm 8, final 10)로 49 to add이고 예약 4개 모두max_size = -1이며, 맞닿는 창 2개와 capacity 11은 validation 오류로 거부됐습니다. #61 이후 기본 plan은 49 to add(uploads bucket 리소스 4개 추가)입니다. apply는 하지 않았습니다.2026-09-02 dev 배포와 무중단 갱신 (#62, #64)
빈 dev state에 release
dev-9596eb99a24f-20260902T091111Z(#62 병합 커밋)를 apply(49 add)해sql-dev.ddps.cloud를 올리고 초기 설정·로그인 화면·Google 인가 URL(hd, dev 콜백 URI)·온보딩 게이트를 스크립트로 확인했습니다. 이어 #64를 병합한 releasedev-1878e147f44a-20260902T113949Z를 같은 환경에 apply(0 add·2 change: launch template AMI, ASG release 태그)했고, instance refresh(Rolling, min healthy 100%, warmup 300초)가 11:50:37Z에 시작해 11:57:25Z에 끝났습니다. 그동안 2초 간격으로/healthcheck·관리자 세션 API·/login을 1,353회 확인해 실패 0이었고, 로그인 세션은 인스턴스 교체 뒤에도 유지됐습니다. 새 인스턴스의 스모크 15개(약관 페이지·푸터 링크 포함) 통과. 브라우저에서 한양 계정이 콜백을 통과하고(userinfo에hd존재), Google 재로그인 시 비밀번호 재설정이 강제되며, Exam Mode의 Trustlock 스위치가 동작하는 것을 확인했습니다.2026-09-02 dev 갱신 2회차 (#66, #67)
release
dev-ededbbd8e7f9-20260902T123043Z를 apply(0 add·3 change: launch template AMI, ASG 태그, 대상 그룹 상태 검사)했고 instance refresh가 12:55:29Z~13:01:59Z에 끝났습니다. 2초 간격 615회 확인에서 실패 0, 세션 유지. 스모크 19개 통과(두 번째 로그인 뒤 첫 세션의 API 401·페이지 로그인 리다이렉트, 두 번째 세션 유지 포함).2026-09-02 시험 무결성 로그 (#68)
플러그인·Google 테스트 34개와 upstream 문제 API 테스트 63개 통과. 테스트 환경에는 judge가 없어 execute 이벤트의 판정은
incorrect(judge 연결 오류)로 남았습니다. 실제 판정 값과 CloudWatch 전송은 dev에서 확인해야 합니다.2026-09-02 dev 갱신 3회차 (#68)
release
dev-d0591bc13f57-20260902T134949Z를 apply(0 add·2 change)했고 instance refresh가 14:00:20Z~14:07:28Z에 끝났습니다. 2초 간격 1,368회 확인에서 실패 0, 세션 유지. 스모크 22개 통과: 관리자 Test 실행이 judgecorrect로 채점되고 CloudWatch behavior 스트림에execute이벤트(source: server)가 도착했으며, 두 번 로그인한 뒤logins스트림에 동시 접속 줄이 남았습니다.2026-09-03 dev 재배포와 갱신 4회차 (#69, #70)
빈 dev state에 release
dev-3a4f60e46b8b-20260903T012606Z(#69 병합)를 apply(49 add)했습니다. 스모크에서 #69 항목이 모두 통과했습니다: 단일 세션 스위치가 꺼진 동안 학생의 이전 세션이 다음 로그인 뒤에도 유지, 켜면 이전 세션이 API 401·페이지는 로그인으로 이동, 관리자 두 세션은 모두 유지, 관리자 페이지에 배너 스크립트 포함; 시험 브라우저 제한을 켠 동안/healthcheck200, 로그인 페이지의 Google 버튼 숨김, 학생 세션 403(API는 JSON), 다른 브라우저의 학생·미등록 이름 로그인 403(기존 학생 세션은 끊기지 않음), Trustlock UA 로그인 통과, 관리자 로그인 통과, 대상 그룹 healthy 유지(#67); Test 실행의execute이벤트(judgecorrect,source: server)와logins의session started줄(직전 로그인 포함). 첨부 파일은 업로드는 됐지만 다운로드가 S3SignatureDoesNotMatch403이어서 #70으로 고쳤습니다. #70 apply(0 add·2 change) 뒤 instance refresh 02:01:55Z~02:08:25Z 동안 2초 간격 531회 확인에서 실패 0, 세션 유지. 다시 돌린 스모크 50개 모두 통과(첨부 다운로드는 S3 리다이렉트를 거쳐 200). 개인 Gmail 거부는 브라우저에서 확인해야 합니다.2026-09-03 dev 갱신 5회차 (#65)
release
dev-9d5807f084a8-20260903T041247Z를 apply(0 add·2 change)했고 instance refresh 04:22:36Z~04:29:04Z 동안 2초 간격 546회 확인에서 실패 0, 세션 유지. 스모크 50개 통과. 약관에 동의하지 않은 학생 계정으로 연 온보딩 페이지(약관 전용 모드)에서 #65의 폼 속성(data-password-min-length·data-password-max-length), 약관 체크박스 표식 1개, 규칙 목록 없음, 실시간 검사 스크립트를 확인했습니다. 입력 중 반응은 브라우저에서 확인해야 합니다.2026-09-03 dev 갱신 6회차 (#71, #72)
release
dev-429224201e73-20260903T054956Z를 apply(0 add·2 change)했고 instance refresh 05:59:21Z~06:05:50Z 동안 2초 간격 507회 확인에서 실패 0, 세션 유지. 스모크 50개 통과. #71 확인: 학생 설정 화면에 Access Tokens 탭 없음, 관리자에게는 있음, 학생POST /api/v1/tokens403, 관리자 토큰 생성·인증·삭제 정상, 로그인 화면 제출 버튼 폼 너비·열 아래 여백 마크업. 화면 모양은 브라우저에서 확인해야 합니다.저장소 테스트
go test,go vet,scripts/test-sql-judge(MySQL 8.4.11 통합 테스트: 격리, 권한, timeout, 스위퍼, 템플릿 호환, HTTP smoke)tests/users tests/oauth tests/plugins test_views test_themes)는 오래 실패하던 회귀 테스트 4건을 포크의 의도된 동작에 맞게 정리 #72 이후 161개 통과, 실패 0입니다.terraform validate, 수정 파일terraform fmt -check운영 반영 시 확인할 것
운영에서도 Test 한 번 → behavior 스트림에
execute이벤트, 로그인 →logins스트림에session started줄이 오는지 확인합니다(dev에서는 확인했습니다). 지난 학기 데이터에는execute이벤트와source·received_at필드가 없습니다.운영 release를 이 PR의 병합 커밋으로 다시 빌드하고
mainchannel을 갱신한 뒤 apply합니다. #56이 포함되지 않은 release로 운영을 시작하면 안 됩니다. 같은 apply가 ASG 설정(시험에 대비한 ASG 설정: 첫 부팅 여유 300초, 예약 스케일, 온디맨드 스케일 아웃 #57)을 in-place로 반영하며 instance refresh 트리거는 tag뿐이라 재배포는 일어나지 않습니다.시험·퀴즈 일정이 정해지면 운영 tfvars의
exam_windows에 창(시험 30분 전 시작, 종료 30분 뒤 끝, capacity 8)을 넣고 plan을 검토한 뒤 apply합니다. 창은 겹치지 않게 하고 capacity는asg_max_size(10) 이하로 둡니다. 시험이 끝나면 항목을 지웁니다. 인스턴스당 채점 상한이 초당 5~8건이라 target tracking만으로는 시험 시작 burst를 따라가지 못합니다.온디맨드 스케일 아웃 기본값(100)은 이 PR에서 제안한 값입니다. spot으로 되돌리려면 tfvars에서
on_demand_percentage_above_base = 0으로 둡니다.문제 내용 수정(조교 작업):
#32GROUP BY, tie-break 없는 6문제의 ORDER BY 보강,#33상관 서브쿼리 최적화, 지문의 숫자·DATE 예시 출력 갱신.scripts/review-challenges --review로 확인합니다.운영 환경의 초기 관리자 설정과 로그인 흐름을 브라우저에서 확인합니다. dev의 브라우저 확인(개인 Gmail 거부, #65의 입력 중 규칙 표시, #71의 화면)은 2026-09-03 사용자가 마쳤고, dev는 06:28Z에 destroy했습니다.
지난 학기 Notion 플랫폼 튜토리얼의 로그인 절차를 새 흐름으로 바꿉니다.
운영 반영 뒤 문제 첨부 파일을 하나 올려 관리자와 학생 계정에서 내려받히는지 확인합니다(S3 첨부 파일 다운로드 링크를 리전 호스트로 서명 (다운로드 403 수정) #70, dev에서는 확인했습니다).