1. 개요
Team Maple Leaf에서 토이 프로젝트로 온라인 저지 플랫폼을 만들어 보았습니다. 총 인원은 5명입니다.
http://www.maple-leaf.dev
2. 개발기간
2022.09.19. ~ 2022.11.30.
약 2.5개월 동안 진행된 프로젝트입니다.
실제 개발 기간은 2022.11.23.까지였고 2022.11.30.까지는 QA및 정리 기간이었습니다.
3. 계획
1) 역할분담
프론트엔드 3명
백엔드 2명
채점서버 2명
저는 프론트엔드, 채점서버 개발을 담당했습니다.
2) 기술
프론트엔드
Vue3 + TypeScript
백엔드
Springboot + MySQL + Redis
채점서버
Rust + RabbitMQ
3) 서버 설정 계획
처음에 계획한 서버 설정입니다. 초반애 채점기능은 다른 오픈소스를 사용할 예정었으므로 채점서버 개발 계획이 없었기 때문에 위 그림에 포함되어 있지 않습니다.
4) 개발방법
매일 22:00에 스크럼 미팅을 진행해서 진행사항 공유 및 질의응답 시간을 가졌습니다. 그리고 팀 노션 페이지에 간단히 회의내용을 정리하였습니다.
4. 진행과정
1) API Documentation 진행 & 프론트엔드 백엔드 개발
백엔드와 프론트엔드 통신에 필요한 url과 port정의 그리고 요청과 응답JSON 형식 정의를 하였습니다.
HTTP 통신을 사용하였고, Swagger를 사용해서 API 문서를 만들었습니다. 작성된 API에 맞춰 프론트엔드와 백엔드 개발 진행을 각각 하였습니다.
프론트엔드는 Vue.js 3와 TypeScript를 사용해서 개발 하였고 패키지 매니저로는 pnpm을 사용했습니다. UI/UX 디자인은 각자 하였고 Vue.js 디자인 라이브러리인 Vuetify를 사용해서 디자인하였습니다.
2) OAuth2.0 기반 인증 시스템 구현
Google로 로그인을 사용하여 사용자 인증을 구현했습니다. 구글 로그인이 성공했을 경우 받아온 토큰에 있는 정보로 유저를 구분하고 이름과 프로필url을 DB에 저장하고 관리했습니다.또한 내부적으로도 인증 토큰과 리프레시 토큰을 발급하여 로그인 유지 기능을 구현하였습니다.인증 토큰은 브라우저 Local Storage에 저장되며 HTTP요청을 보낼 때 같이 전송되어 스프링 시큐리티에서 유효성 검증을 하고 다음 프로세스가 진행 됩니다.
3) SSL인증서 작업
Let's Encrypt를 사용하여 인증서 등록을 하여 HTTPS 통신이 가능합니다.
4) 채점서버 개발
채점서버는 Rust로 만들어졌습니다. 백엔드와는 2개의 메시지 큐로 연결되어 있습니다. 하나는 채점큐, 다른 하나는 결과 큐입니다.
채점서버는 단순히 소스코드를 컴파일하고 실행하는 것 뿐만 아니라 시스템 접근도 차단해야 하고 시간제한과 메모리제한을 설정해야 했습니다. 또한 Rust언어로 만들었기 때문에 자료도 부족하고 새로 배우면서 채점 서버를 만들어야 했기에 시간이 더 오래 걸렸습니다.
시스템 콜 차단은 Rust 라이브러리 중에 seccomp 설정을 할 수 있는 seccompiler가 있어 이것을 사용했습니다. seccomp는 linux에서 동작하기 때문에 Docker 컨테이너에서 실행하는 것으로 결정했습니다.
시간 제한과 메모리 제한은 Rust에 libc가 있었기때문에 setrlimit으로 설정할 수 있었습니다. 또한 시간 사용량과 메모리 사용량도 libc의 getrusage로 가져올 수 있었습니다.
백엔드와는 AMQP로 통신하는 RabbitMQ를 사용해 소스코드와 테스트케이스 데이터, 채점 데이터를 주고받았습니다.
5) 프론트엔드 백엔드 채점서버 통합
프론트엔드는 Vercel에서 호스팅 되고 있고 백엔드는 Mac Mini에서 호스팅되고 있습니다. 프론트엔드와 벡엔드는 통합되어 올라가 있으나 채점서버는 아직 연결작업 진행중입니다.
6) CI,CD 설정
CI는 github workflow로하였습니다. build 테스트를 위한 YAML 파일을 생성하고 github에 같이 올려놓았습니다.
CD는 처음에 jenkins로 시도하였으나 docker-compose에 문제가 발생하여 jetbrain의 teamcity를 사용했습니다. Mac Mini에 teamcity를 설치해놓고 CD 설정을 하였습니다.
5. 문제해결
1) docker image 파일 크기 줄이기
기존 도커파일은 다음과 같았는데 이렇게 도커 빌드를 했을 때 이미지의 크기가 2.2GB에 가깝게 나오는 문제가 있었습니다.
FROM ubuntu:latest
WORKDIR /usr/local/bin/online_judge
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "gcc"]
RUN ["apt-get", "install", "-y", "libseccomp-dev"]
RUN ["apt-get", "install", "-y", "curl"]
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN ["rustc", "--version"]
COPY . .
RUN ["cargo", "build", "--release"]
RUN rm -rf target/deps/online_judge*
CMD ["./target/release/online_judge"]
Rust를 통째로 설치해서 용량이 커지는 문제가 발생했습니다. 그래서 빌드를 다른 곳에서 하고 바이너리 파일만 컨테이너 위에 올리도록 수정하였습니다. 도커 이미지 용량이 200MB정도까지 줄었습니다.
FROM rust:alpine AS chef
WORKDIR /usr/src/online_judge
RUN set -eux; \
apk add --no-cache musl-dev; \
cargo install cargo-chef; \
rm -rf $CARGO_HOME/registry
FROM chef as planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
RUN apk add --no-cache libseccomp-dev
COPY --from=planner /usr/src/online_judge/recipe.json .
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release
FROM alpine:latest
WORKDIR /usr/local/bin
RUN apk add --no-cache build-base
COPY --from=builder /usr/src/online_judge/target/release/online_judge .
COPY ./result ./result
COPY ./test_cases ./test_cases
CMD ["./online_judge"]
또한 cargo chef를 사용해서 사용했던 dependencies를 캐싱하여 빌드할 때마다 시간이 너무 오래 걸렸던 문제도 해결하였습니다.
2) seccomp라이브러리 문제
처음에는 seccomp-sys를 사용하여 개발 하였는데 컴파일 할 때 libseccomp 오류를 해결할 수가 없었습니다.
error: linking with `cc` failed: exit status: 1
|
= note: "cc" "-m64" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/rcrt1.o" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crti.o" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtbeginS.o" "/tmp/rustc4mImUQ/symbols.o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824.1g6ao5lqz68y85o3.rcgu.o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824.20l2y7y607if5qqb.rcgu.o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824.3lsqmia6m04mc96f.rcgu.o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824.4e42iiepqxna0m48.rcgu.o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824.8ddup0wzlnwcn2d.rcgu.o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824.4mno65psuhyzujr3.rcgu.o" "-Wl,--as-needed" "-L" "/app/target/debug/deps" "-L" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib" "-Wl,-Bstatic" "/app/target/debug/deps/libseccomp_sys-d8932ff617be4a9a.rlib" "/app/target/debug/deps/liblibc-ba8e0e4476b94e18.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libstd-40a812d8a2d60455.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libpanic_unwind-e1ffe88dc2f5f25f.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libobject-fa4b51db4a921a45.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libmemchr-1c916089a8e606a0.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libaddr2line-1769f6147ee85248.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libgimli-5a0c9d8cb3ff1d5e.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/librustc_demangle-04049f56a9535c98.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libstd_detect-44b0a68c2c184b7b.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libhashbrown-4f318d60a5f279ee.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libminiz_oxide-bc03d80400427e1d.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libadler-357b3d05ebedb664.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/librustc_std_workspace_alloc-2424e0af2fae3941.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libunwind-fb07c22de92ba98d.rlib" "-lunwind" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libcfg_if-8fc19461d9a0d5ac.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/liblibc-785f760c3dc624bb.rlib" "-lc" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/liballoc-47f5da452a196927.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/librustc_std_workspace_core-3ce4def87b0fb72b.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libcore-0537809a006e1951.rlib" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/libcompiler_builtins-f89a9b812c54a61c.rlib" "-Wl,-Bdynamic" "-lseccomp" "-Wl,--eh-frame-hdr" "-Wl,-znoexecstack" "-nostartfiles" "-L" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib" "-L" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained" "-o" "/app/target/debug/deps/seccomp_test-952dd4ec542f5824" "-Wl,--gc-sections" "-static-pie" "-Wl,-zrelro,-znow" "-nodefaultlibs" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtendS.o" "/usr/local/rustup/toolchains/1.65.0-x86_64-unknown-linux-musl/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtn.o"
= note: /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: cannot find -lseccomp: No such file or directory
collect2: error: ld returned 1 exit status
의외로 간단히 해결하였는데 다른 seccomp라이브러리로 교체하니까 해결되었습니다. 그래서 Seccompiler를 사용하는 것으로 결정하였습니다.
3) x86_64와 Aarch64 시스템콜의 차이
차단할 systemcall을 몇개 리스트를 찾아 x86_64 환경에서 적용하고 동작 하는지 확인을 한 다음 M1맥북에서 pull 받아서 실행해보았는데 오류가 나고 실행이 되지 않았습니다.
https://docs.rs/crate/libc/latest/source/src/unix/linux_like/linux/musl/b64/x86_64/mod.rs
libc 0.2.137 - Docs.rs
libc 0.2.137 Raw FFI bindings to platform libraries like libc.
docs.rs
https://docs.rs/crate/libc/latest/source/src/unix/linux_like/linux/musl/b64/aarch64/mod.rs
libc 0.2.137 - Docs.rs
libc 0.2.137 Raw FFI bindings to platform libraries like libc.
docs.rs
라이브러리 docs를 확인해봤더니 x86_64에는 open이 있지만 aarch64에는 존재하지 않았습니다. 시스템콜 이름은 공유되는게 아니었나 싶어 리눅스 공식 깃허브에 가서 찾아봤는데 실제로 다른것이었습니다. (이거 빼고 다 똑같은거 같습니다.) aarch64위에서 채점 서버가 돌아갈 것이기 때문에 aarch64기준으로 시스템콜 차단 리스트를 만들었습니다.
6. 결과
현재 프론트와 백엔드만 연결되어있는 상태로 마무리 되었습니다. 12월 13일 이후로 마무리 작업을 마저 하기로 결정되었습니다.
7. 배운점 및 느낀점
1) git flow
이번에 git branch 전략 몇 가지를 배워보고 git flow를 사용해 보았습니다. main, develop, feature, release 브랜치를 만들어서 관리하는 것이었습니다.
각 레포지토리마다 2명이나 3명밖에 없어서 그림처럼 복잡한 로그가 생기지는 않았습니다만 협업을 할 때 브랜치가 너무 복잡해지면 관리하기 힘들기 때문에 적절한 브랜치 전략을 선택하는 것이 중요하다는 생각을 했습니다.
2) Docker & docker-compose
전에는 docker라는 것이 단순하게 개발 환경까지 함께 묶어서 관리하는구나 정도로만 알고 있었는데 이번에 직접 사용해 보면서 docke와 docker-compose에 대해 많이 이해하게 되었습니다.
3) 새로운 언어들과 프레임워크
Rust, Vue.js, TypeScript를 직접 사용해보고 배워보았습니다.
마치며
지금까지 해본 것들은 로컬에서 돌아가는것만 만들어 보는게 전부였는데 실제로 배포까지 해보았습니다. 그 과정에서 처음 해보는 것들 투성이라 정신없이 공부만 하고 이것저것 맞는지 틀리는지도 모르는 채로 개발해 보았던 것 같습니다. 하지만 그만큼 배워가는 것도 많고, 함께 배워가면서 도움도 많이 받았습니다.