사용자 Machine에 연결하기
최종 사용자를 위해 Machine을 운영할 때 흔히 마주치는 과제는 요청을 이 Machine들로 효율적으로 관리하고 라우팅하는 것이에요. 이 가이드에서는 Fly.io에서 사용자 Machine에 연결하기 위한 권장 접근 방식을 설명해 드릴게요. HTTP 서비스 구성부터 fly-replay 활용, 인증, 안티패턴, 예제 서비스 구성까지 한 번에 살펴볼 수 있어요.
출처: 문서
본문
개요
최종 사용자를 위해 Machine을 운영할 때 흔한 과제는 요청을 이 Machine들로 효율적으로 관리하고 라우팅하는 것이에요. 이 가이드는 Fly.io에서 사용자 Machine에 연결하기 위한 권장 접근 방식을 설명해 드려요.
전형적인 스택
- Coordinator: 최종 사용자 대신 Machine을 관리하는 앱이에요. Router 역할도 할 수 있어요.
- Router: 요청을 사용자 Machine으로 중계하는 앱이에요. Coordinator와 동일할 수 있어요.
- User App: 격리된 네트워크에서 특정 사용자에게 할당된 앱이에요.
- User Machine: 그 앱들의 Machine이에요.
HTTP 서비스
전형적인 구성은 두 가지 서비스를 포함해요:
- 관리(management) HTTP 서비스(
port 9090): 다음 같은 작업을 위한 서비스예요.- LLM 생성 코드 변경 전송
- 헬스 체크
- 메트릭 요청
- 공개(public) HTTP 서비스(
port 4443): 다음을 위한 서비스예요.- 라이브 프리뷰(Live Previews)
- LLM <-> MCP 서버 연결
- 배포된 사용자 앱
HTTP 요청 처리
이 다이어그램은 HTTP 요청이 처리되는 방식을 보여줘요:
- 공개 HTTP 요청은 라우터 앱에 도착하고, 라우터는 특정 사용자 앱으로 fly-replay를 발행해요. 그러면 Fly Proxy가 요청을 대상 앱으로 다시 재생(replay)하며, 인바운드 요청 포트를 기준으로 서비스를 매칭해요.
- 프라이빗 HTTP 요청은 인터넷에서 접근할 수 없는 내부 관리 게이트웨이에 도착해요. 동일한 재생 과정이 요청을 지정된 포트의 사용자 앱으로 보내며, 인바운드 요청 포트를 기준으로 서비스를 매칭해요.
fly-replay 사용하기
HTTP로 사용자 Machine에 연결하기 위한 권장 메커니즘은 fly-replay 응답 헤더예요. 이 방식은 글로벌 프록시가 요청을 특정 Machine으로 다시 라우팅하도록 지시하는 방식으로 작동해요. Machine이 실행 중이지 않으면 프록시가 시작해요. 이 접근 방식은 WebSocket을 포함한 모든 종류의 HTTP 요청에 적용돼요. 프록시가 전체 요청을 관리하므로, 라우터 앱이 fly-replay 헤더를 보낸 뒤에는 더 이상 실패 지점이 되지 않아요.
포트 매칭
fly-replay를 사용할 때 프록시는 들어오는 요청의 포트를 대상 서비스의 공개 포트와 매칭해요. 예를 들어:
- 포트 80에 대한 요청은
port: 80으로 구성된 서비스로 라우팅돼요. - 포트 9090에 대한 요청은
port: 9090으로 구성된 서비스로 라우팅돼요.
즉, 라우터 앱이 대상 서비스가 사용하도록 구성된 것과 동일한 포트에서 요청을 받아야 한다는 뜻이에요. 예를 들어 포트 9090의 관리 API에 연결하려면 라우터가 포트 9090에서 요청을 받아야 해요.
프라이빗 네트워크 라우터 예제
private-network-router 예제는 여러 서비스를 처리할 수 있는 라우터를 구축하는 방법을 보여줘요. 관리 API를 프라이빗으로 유지하면서도 라우팅을 허용하는 데 flycast를 사용해요. 라우터는:
- 어떤 포트에서든 요청을 받아요.
- 요청에 따라 대상 앱과 Machine을 결정해요.
app,instance같은 매개변수가 포함된fly-replay응답 헤더를 보내 요청을 올바른 앱과 Machine으로 라우팅해요. 매개변수에 대한 자세한 내용은 fly-replay 문서를 참고하세요.
이 패턴을 사용하면:
- 관리 API를 flycast로 실행해 프라이빗으로 유지할 수 있어요.
- 단일 라우터에서 여러 서비스로 요청을 라우팅할 수 있어요.
- 관리 포트를 인터넷에 직접 노출하지 않아 보안을 유지할 수 있어요.
인증
재생된 요청의 검증은 사용자 Machine 안에서 이루어져야 해요. 이 목적으로 fly-replay의 state 메커니즘을 사용하는 것을 권장해요:
- 각 사용자 앱에 대해 사전 공유 키(preshared key)를 만들고 앱 시크릿으로 설정해요.
- 재생을 발행할 때 헤더를 설정해요:
fly-replay: app=<app>,state=<key>. - 사용자 Machine에서
fly-replay-src헤더를 파싱해요. 이 헤더에는instance,region,t,state같은 필드가 포함돼요 (fly-replay 문서 참고). fly-replay-src의state값을 앱의 사전 공유 키와 대조해요. 일치하면 요청이 검증돼요.
이 검증의 실제 구현은 아래 TypeScript 예제를 참고하세요.
안티패턴
- flycast, <app>.fly.dev, 전용 IP(Dedicated IPs): 이들은 서로 통신해야 하는 영구 앱을 위한 것이에요. 이 방식은
fly-replay로 라우팅하는 것보다 관련 요소가 더 많아요. 요소가 많을수록 문제가 생길 가능성도 커져요. - TCP 또는 UDP: 최종 사용자 Machine과 통신할 때 원시 TCP나 UDP를 피하세요. 필요한 경우 코드와 함께 Tailscale 컨테이너 실행을 고려해 보세요.
예제 서비스 구성
관리 서비스와 표준 공개 HTTP 서비스를 모두 포함하는 Machine의 예제 서비스 구성이에요:
{
"services": [
{
"name": "management",
"internal_port": 9090,
"ports": [
{
"port": 9090,
"handlers": ["http"]
}
]
},
{
"name": "public",
"internal_port": 8080,
"ports": [
{
"port": 80,
"handlers": ["http"]
}
]
}
]
}
이 구성은 다음을 설정해요:
- 내부적으로 포트 9090에서 수신하고 포트 9090으로 노출되는 관리 서비스
- 내부적으로 포트 8080에서 수신하고 포트 80으로 노출되는 공개 서비스 두 서비스 모두 필요할 때 자동으로 시작되고 유휴 상태가 되면 중지돼요.
예제: TypeScript에서 재생된 요청 인증하기
fly-replay-src 헤더와 사전 공유 키를 사용해 TypeScript HTTP 핸들러에서 재생된 요청을 인증하는 간단한 예제예요:
// Example preshared key for the app (in practice, load from env or secret store)
const PRESHARED_KEY = process.env.PRESHARED_KEY;
function parseFlyReplaySrc(header: string | undefined) {
if (!header) return {};
return Object.fromEntries(
header.split(',').map(pair => {
const [key, value] = pair.split('=');
return [key.trim(), value?.trim()];
})
);
}
async function authenticateReplay(request: Request): Promise<boolean> {
const flyReplaySrc = request.headers.get('fly-replay-src');
const params = parseFlyReplaySrc(flyReplaySrc);
return params.state === PRESHARED_KEY;
}
// Example usage in a fetch handler
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request: Request): Promise<Response> {
if (!(await authenticateReplay(request))) {
return new Response('Unauthorized', { status: 401 });
}
// ...handle the authenticated request...
return new Response('OK');
}
이 코드는 fly-replay-src 헤더를 확인하고, state 값을 파싱한 뒤 사전 공유 키와 비교해요. 일치하면 요청이 인증돼요.
관련 자료
- Fly Machines로 사용자별 개발 환경 만들기 각 사용자를 위한 격리된 온디맨드 환경을 실행하는 방법을 알아보세요.
- 사용자 앱 관측성 트래픽을 라우팅한 뒤 사용자별 앱/Machine에서 로그와 텔레메트리를 수집하는 방법을 알아보세요.
- fly-replay로 동적 요청 라우팅
fly-replay헤더와 동적 요청 라우팅에 대해 알아보세요.