최근 원격지원 서비스를 개발과 동시에 FRP Github Contributor가 된 경험을 작성하고자 한다.
1. FRP 적용 이유
우리 회사의 서비스 제품 중 일부는 데모 클라이언트 역할을 하는 서버들이 포함되어 있으며, 이러한 서버들은 외부 노출을 위해 Ngrok을 사용하고 있다.
NGROK 이란?
로컬에서 실행 중인 서버를 인터넷에서 접근할 수 있도록 안전한 터널을 제공하는 툴이다.
설정이 간단하다는 장점이 있지만, 속도와 기능이 유료버전에 비해 제한적이라는 단점이 존재한다.
LiDAR와 함께 패키지 형태로 제공되는 서비스는 대부분 클라이언트의 폐쇄망에서 사용된다. 하지만, 긴급 유지보수나 이슈 확인을 위해 NAT(Network Address Translation)를 우회해야 할 경우, 기존에는 RustDesk와 Ngrok 같은 외부 도구를 활용했다.
그러나 이 환경은 여러 문제점을 가지고 있었다:
• RustDesk: 원격 제어 툴로는 부족한 성능과 안정성.
• Ngrok: 속도와 기능이 제한적.
• 터미널 환경: 원활하지 않은 연결로 인해 이슈 확인이 비효율적.
개선 필요성
이러한 문제를 해결하기 위해:
1. 외부 소프트웨어 의존도를 낮추고,
2. 더 나은 성능과 커스터마이징 옵션(소스코드와 동일한 언어)을 제공하며,
3. 원격 지원 및 터널링 기능을 하나로 통합한 설루션이 필요했다.
이에 따라, 우리는 FRP(Fast Reverse Proxy)를 적용하기로 결정했고, 아래와 같은 형태로 중계서버를 두고 중계 서버에서는 내부 네트워크망을 연결할 수 있는 방식의 형태를 이용해서 내-외부망의 통신을 가져갔다.
기존 구조에서 중계 서버는 FRP로 대체되었으며, 제공되는 데이터 수집 서버에 FRP 클라이언트를 설치하여 클라이언트 요청에 따라 중계 서버와 연결/해제가 가능한 형태로 설계했으며, 아래 그림과 같다.
1. 중계 서버 (FRP):
• 외부망과 내부망을 연결하는 브릿지 역할.
• 데이터 수집 서버의 클라이언트 요청을 처리하여 중계 서버와 연결하거나 해제할 수 있는 동적 환경 제공.
2. 내부망:
• 데이터 수집 서버는 FRP 클라이언트를 통해 중계 서버와 연결.
• 내부망에는 엔지 서버 및 LiDAR 센서가 위치하며, 데이터를 실시간으로 처리 및 제공.
3. AWS Private 환경:
• AWS 환경에 ALB(Application Load Balancer)와 Lambda를 활용하여 자동화된 데이터 흐름 관리.
• Lambda는 요청 처리를 통해 CloudWatch와 연동하여 로그 및 알림을 관리.
4. Slack Notification:
• FRP의 연결 상태나 요청 이벤트는 Slack 알림을 통해 실시간으로 전달.
• 운영자가 원격 연결 상태를 즉시 확인할 수 있도록 설계.
기존 RustDesk, Ngrok의 역할은 Frp가 대체하고 해당 연결의 Notification을 Slack을 통해 알람을 전달하는 구조를 생각했다.
- Lambda 함수의 관리를 위한 Chalice, LoadBalancer는 다음 포스팅에 다루고 FRP 위주로 작성해보고자 한다.
2. FRP 적용방법
기존 서버 환경은 Docker Container 환경으로 구축되어 있기 때문에 FRP에서 제공하는 Server, Client를 이용했다.
우선 FRPS의 설정부터 확인해 보면 다음과 같다.
bindPort = 7000
webServer.addr = "0.0.0.0"
webServer.port = 9500
webServer.user = "admin"
webServer.password = "admin"
transport.tcpMux = true
tcpmuxHTTPConnectPort = 1337
log.to = "console"
log.level = "trace"
log.maxDays = 3
1. bindPort:
• 중계 서버에서 외부망(public)으로부터 접근 가능한 포트를 설정.
• 해당 포트는 방화벽 또는 라우터에서 포트 포워딩이 설정되어 있어야 외부에서 접근이 가능.
2. transport.tcpMux:
• TCP Multiplexing 기능을 활성화하여 SSH 터널링과 같은 다중 연결을 효율적으로 처리할 수 있도록 설정.
• tcpmuxHTTPConnectPort: Multiplexing에 사용할 포트를 지정합니다. 이 포트는 SSH 터널링에 매우 중요하다.
3. webServer.addr & webServer.port:
• FRPS 관리 페이지(Web UI) 접근 주소와 포트 설정.
• 사용자 인증을 위해 webServer.user와 webServer.password를 지정.
4. log 설정:
• 로그 출력 대상과 수준(trace, debug, info 등)을 지정.
• log.maxDays는 로그 보관 기간을 설정.
회사 개발 서버에서 포트 포워딩을 통해 bindPort(7000)가 외부망에서 접근 가능하도록 라우터를 설정했다.
다음 FRPC의 설정을 확인해보면 다음과 같다.
user = "frp_test"
serverAddr = "외부 공개된 중계서버 IP"
serverPort = 설정된 포트 번호
log.to = "console"
log.level = "debug"
# Set admin address for control frpc's action by http api such as reload
webServer.addr = "127.0.0.1"
webServer.port = 9500
webServer.user = "admin"
webServer.password = "admin"
[[proxies]]
name = "ssh"
type = "tcpmux"
multiplexer = "httpconnect"
localPort = 22
localIP = "127.0.0.1"
customDomains = ["abc.com"]
[[proxies]]
name = "a"
type = "http"
localPort = 11111
customDomains = ["abc.com"]
locations = ["/api/v1/a"]
[[proxies]]
name = "b"
type = "http"
localPort = 22222
customDomains = ["abc.com"]
locations = ["/api/v1/b"]
[[proxies]]
name = "c"
type = "http"
localPort = 33333
customDomains = ["abc.com"]
1. serverAddr & serverPort:
• FRPS(Server)의 공인 IP와 bindPort를 설정합니다.
2. proxies:
• 중계 서버에서 전달할 요청의 규칙을 정의합니다.
• SSH 터널링: type = "tcpmux"를 사용하여 특정 포트(localPort 22)를 지정.
• HTTP 프록시: type = "http"를 사용하여 경로(/api/v1/a, /api/v1/b)에 맞는 요청을 특정 포트로 전달.
3. customDomains:
• 요청을 받을 도메인 이름을 정의합니다.
• abc.com이라는 도메인에 대해 특정 경로 요청을 설정한 포트로 전달하도록 구성했습니다.
그 외에는 location에 맞는 라우팅이 되는 것이 마치 프록시 설정과 상당히 비슷하다.
FRP는 중계서버에서 리버스 프록시를 해주고 있는 것이다.
예를 들어, 클라이언트가 abc.com/api/v1/a에 접속하면:
• FRPS는 해당 요청을 FRPC로 전달한다.
• FRPC는 로컬 IP(127.0.0.1)의 포트 11111로 요청을 라우팅 한다.
이 방식은 마치 리버스 프록시 설정과 유사하며, 클라이언트의 요청을 내부망으로 안전하게 전달할 수 있다.
SSH 터널링의 경우 proxy socat을 활용하여 로컬 -> 중계서버 -> 클라이언트서버로 갈 수 있도록 아래와 같은 커맨드를 활용했다.
ssh -o 'ProxyCommand socat - Proxy:127.0.0.1:abc.com:22,proxyport=1337' xxxx@중계서버.com
1. ProxyCommand란?
ProxyCommand는 SSH 클라이언트 옵션으로, SSH 연결을 설정하기 전에 특정 명령어를 실행하도록 지정한다.
이를 통해 SSH가 직접 대상 서버에 연결하지 않고, 프록시(Proxy)나 다른 중계 프로그램을 경유하도록 설정할 수 있다.
2. socat - Proxy:란?
socat은 소켓 통신을 다루는 강력한 유틸리티입니다. 다양한 프로토콜(예: TCP, UDP, HTTP)을 사용하여 데이터를 전송하거나, 소켓 연결을 프록시 서버를 통해 중계하는 데 사용됩니다.
1337은 기존 중계서버에서 tcpmux의 openport임을 기억하자.
3. Customize
클라이언트 환경 최적화
• 대부분의 클라이언트는 교통량이 많은 교차로에서 소형 PC에 설치되어 작동하고 있다.
• 제한된 디스크 용량과 메모리를 고려해, 최적화 가능한 부분을 식별하고 개선 작업을 진행했다.
TinyFRP 적용
• TinyFRP라는 경량화된 FRP 클라이언트를 도입하여 기존 20MB였던 클라이언트 크기를 약 6MB로 축소(95% 용량 절감).
• 용량 절감이 가능했던 이유:
• 필요 없는 Web Admin 페이지 제거.
• JSON 및 TOML 파싱 기능 제거.
의존성 관리 문제와 해결
FRP 클라이언트를 라이브러리 화하는 과정에서 go mod 의존성 관리 문제가 발생했다:
1. FRP가 사용하는 samber.io 라이브러리의 버전업으로 인해 코드 충돌이 발생.
2. go.mod의 replace를 통해 문제를 해결할 수 있었으나, 자동화된 배포 과정에서 스크립트 관리 문제가 발생할 가능성이 있다고 판단.
PR(Pull Request)로 해결
• 장기적인 안정성을 위해 FRP 라이브러리에 Pull Request를 작성하여 수정 사항을 기여했다.
• 수정 사항이 프로젝트의 Main 브랜치에 Merge 되었을 때, 개발자로서 가장 기분 좋았던 순간 중 하나였다.
4. 결과
비용 절감
1. 기존에 사용하던 Ngrok을 제거하여 매월 31달러의 비용 절감을 달성.
2. 모든 클라이언트 구역에 FRP가 적용되면, 월 300달러 이상의 비용 절감이 예상된다.
안정성 및 효율성 향상
• 기존 터미널 환경에서 발생했던 끊김 현상이 크게 개선되어, 운영 환경의 스트레스를 줄였습니다.
• VOC 처리와 모니터링 환경이 Slack 알림 연동으로 더욱 효율적으로 변화.