
CloudSquare 현장실습 2차 과제로, 나라장터에서 실제 공공 RFP를 하나 골라 요구사항에 맞는 아키텍처를 직접 구현하고 발표하는 과제였습니다.
호서대학교 2026학년도 LMS 클라우드 유지보수 과제를 선택해서 Naver Cloud Platform(NCP) 위에 전체 인프라를 구축해봤습니다.
| 항목 | 내용 |
|---|---|
| 클라이언트 | 호서대학교 (Hoseo University) |
| 프로젝트명 | 2026학년도 LMS 클라우드 유지보수 |
| 기간 | 2026.03.01 ~ 2027.02.28 (12개월) |
| LMS 플랫폼 | Coursemos LMS (㈜유비온) |
| 클라우드 | Naver Cloud Platform (NCP) |
| LMS URL | https://learn.hoseo.ac.kr/ |

| 분류 | 기술 | 선택 이유 |
|---|---|---|
| OS | Rocky Linux 8 | RHEL 계열, 공공기관 표준 |
| Web Server | Apache httpd | RFP 명시, mod_ssl 내장 |
| WAS | Tomcat 10.1.18 | Coursemos LMS 서블릿 기반 |
| Runtime | Java OpenJDK 17 | LTS, 무료 라이선스 |
| DBMS | MySQL 8.0 | RFP 명시, utf8mb4 한국어 완전 지원 |
| Cache | Redis 7.x | 세션 캐시, 응답시간 SLA 대응 |
| Source Control | Gitea 1.21.4 | 내부망 자체 호스팅 (SER-010) |
| 모니터링 | Prometheus + Grafana | Pull 방식, Node Exporter 연동 |
| CDN | NCP Global Edge | 동영상/정적 리소스 배포 |

Apache가 앞단에서 HTTP/HTTPS 요청을 받고, 동적 요청은 뒤에 있는 Tomcat으로 넘기는 구조입니다.

| 소프트웨어 | 역할 |
|---|---|
| Apache httpd | 정적 리소스 처리, SSL(HTTPS) 담당 |
| Java OpenJDK 17 | Tomcat 런타임 |
| Tomcat 10.1.18 | LMS 동적 요청 처리 (DB 조회, 세션, 비즈니스 로직) |
| fail2ban | 반복 로그인 실패 시 자동 IP 차단 |
서비스는 httpd, tomcat 모두 systemd에 등록해서 재부팅 후에도 자동 시작되도록 구성했습니다.
RFP 과업지시서에 "WEB 서버 기술지원" 항목이 Apache 기준으로 명시되어 있고, 국내 공공·교육기관에서 사실상 표준으로 사용
Apache + Tomcat은 mod_proxy_ajp로 연동하는 검증된 구성으로 레퍼런스가 많음
mod_ssl 내장으로 HTTPS 처리를 별도 솔루션 없이 httpd 자체에서 처리 가능
LTS(Long Term Support) 버전으로 12개월 유지보수 기간 동안 보안 패치 보장
Oracle JDK는 상용 환경에서 유료인 반면, OpenJDK는 라이선스 비용 없음
Tomcat 10.1은 Java 11 이상을 요구하며, 17이 그 중 최신 LTS
Coursemos LMS가 Java 서블릿 기반이므로 서블릿 컨테이너가 필수이고, Tomcat이 사실상 표준
10.1.x는 Jakarta EE 10 기반으로 최신 Java 표준을 지원하면서도 충분히 검증된 안정 버전
WEB/WAS 서버가 2대이기 때문에 LMS 업로드 파일(강의 자료, 첨부파일 등)을 두 서버가 동일하게 바라봐야 합니다.
각 서버 로컬 디스크에 저장하면 #1에 올린 파일을 #2에서 못 읽는 문제가 생기기 때문에
NCP NAS를 NFS로 마운트해서 공유 스토리지로 해결했습니다.

보안 설정과 성능 튜닝이 핵심이었습니다.
# 성능 튜닝 (8vCPU / 32GB 기준)
innodb_buffer_pool_size = 20G
innodb_buffer_pool_instances = 8
innodb_log_file_size = 1G
max_connections = 500
익명 사용자 제거, test DB 삭제 (SER-004)
require_secure_transport = ON 으로 SSL 암호화 전송 강제 (SER-005)
root 원격 접근 차단, localhost만 허용 (SER-011)
매일 새벽 3시 mysqldump 자동 백업 → 30일 보존 (FUR-001)

Object Storage 백업 파일 연동


바인드 주소는 10.0.1.7, 127.0.0.1 (내부망 전용)으로 설정했습니다.
또한, 최대 메모리: 4GB (allkeys-lru 정책, 8GB RAM의 50%) 로 설정했습니다.
allkeys-lru 정책
Redis 메모리가 꽉 찼을 때 가장 오랫동안 사용되지 않은 키를 자동으로 삭제하는 정책.
LRU = Least Recently Used (가장 최근에 덜 쓰인 것)
Redis를 세션/페이지 캐시 용도로 쓸 때는 오래된 데이터보다 최신 데이터가 더 중요하기 때문에 lru 정책을 적용했습니다.
bind의 의미를 짚고 넘어가면, "이 서버의 어떤 NIC에서 수신 대기할 것인가" 입니다.
10.0.1.7은 App Tier NIC,127.0.0.1은 loopback.
보안 설정으로는 openssl rand -base64 32로 생성한 패스워드 적용,
FLUSHALL / FLUSHDB / CONFIG 등 위험 명령어 rename-command로 비활성화했습니다.

Gitea
Git 기반 자체 호스팅 소스코드 저장소.
LMS 설정파일, 스크립트, 인프라 코드 버전 관리.
Gitea는 GitHub 대신 쓰는 이유가 명확합니다.
[5개 서버] → Node Exporter(:9100) → Prometheus(:9090) → Grafana(:3001)
15초 pull 방식
Prometheus의 Pull 방식은 수집 주체가 Prometheus 쪽입니다.
각 서버에 Node Exporter만 설치해두면 Prometheus가 알아서 15초마다 긁어오게 됩니다.
Zabbix 같은 Push 방식과 반대 개념입니다.

위 사진은 Grafana 대시보드 사진입니다.

SSH 포트 변경: 22 → 2222
root 원격 접근 차단
MySQL localhost만 허용
관리자 접근 SSL VPN 경유 필수
ACG 서버별 최소 권한 원칙 적용
Redis 접근 제어: IP 제한 + 패스워드 + 위험 명령어 비활성화
SSH 포트를 2222로 바꾼 이유는, 포트 스캐닝 기반 자동화 공격 대부분이 22번을 타깃으로 하기 때문에 포트 변경만으로도 노이즈성 공격을 상당히 줄일 수 있습니다.
여기에 SSL VPN 경유 정책을 더했습니다.

MySQL 8.0은 기본적으로 validate_password 플러그인이 활성화되어 있어서 비밀번호에 대문자, 숫자, 특수문자가 포함되어야 합니다.
소문자로만 설정하려다 오류가 났고, 대문자를 포함한 비밀번호로 변경해서 해결했습니다.
NCP 서버는 기본적으로 PermitRootLogin prohibit-password로 설정되어 있어서 패스워드 기반 root 로그인이 막혀있습니다.
SSH 클라이언트에서 접속 시도하면 Permission denied가 뜨는데,
NCP 콘솔의 터미널(웹 터미널)에서 직접 /etc/ssh/sshd_config를 열어 PermitRootLogin yes로 변경한 뒤 sshd를 재시작해서 해결했습니다.
Prometheus가 각 서버의 9100 포트로 메트릭을 수집하는 구조인데, ACG에 9100 포트를 열어두지 않아서 수집이 안 됐습니다..
Dev 서버 ACG 아웃바운드에 9100을 추가하고, 각 서버 ACG 인바운드에도 Dev 서버(10.0.2.6)에서 오는 9100 트래픽을 허용하도록 추가해서 해결했습니다.
실제 나라장터 RFP를 기반으로 구현하다 보니, 단순 실습과는 다르게 요구사항 번호(FUR, SER, PER)에 맞춰 근거를 가지고 설계를 해야 해서 좋았습니다.
특히 "왜 이 서비스를 선택했나"를 요구사항과 연결해서 설명하는 연습이 됐습니다.
다음 단계로 개선하고 싶은 부분은 DB 이중화와 Alert 기능을 사용해 Mattermost, Slack 등 메신저 앱에 알림기능을 추가하는 것입니다.
Grafana 모니터링은 구축했는데 알림은 아직 미완성이라 아쉬웠습니다.
정택준
Email taekjunnnn@gmail.com
Team https://nangman.cloud