Deploy Rust Code Faster

Matthias Endler

Rust 코드를 더 빠르게 배포하기

기술 여정을 돌아보면 베어메탈 서버를 다루던 시절부터 클라우드 컴퓨팅의 세계를 탐험하기까지 꽤 먼 길을 걸어왔습니다. 처음에는 너무나 간단해 보였습니다. 서버 하나 띄우고 컨테이너 하나 배포하면 끝이니까요. 하지만 더 깊이 파고들수록 인프라의 편리함이 겉보기만큼 단순하지 않다는 것을 깨닫게 되었습니다.

클라우드 제공업체는 저마다 학습 곡선을 요구하는 수많은 도구를 제공합니다:

  • Google Cloud / AWS
  • Kubernetes
  • Helm
  • Docker
  • Terraform
  • GitHub Actions

모험심이 있다면 EKS나 GKE 같은 관리형 쿠버네티스 서비스에 도전해 볼 수도 있습니다. 클릭 몇 번이면 애플리케이션을 바로 실행할 수 있으니 참 매력적입니다. 하지만 모니터링, 로깅, 보안, 스케일링 등을 동시에 챙기기 시작하면 현실이 다가옵니다.

어느새 제품에 집중하는 대신 의도치 않게 DevOps 팀을 이끌고 있는 자신을 발견하게 됩니다. 인프라를 관리하기 위해 직원을 더 뽑는 동안 경쟁사는 기능을 출시하고 사용자 기반을 늘려가고 있습니다.

좌절감

클라우드는 인프라를 쉽게 만들어 주겠다고 약속했지만, 쏟아지는 도구와 서비스는 오히려 압도적입니다. 모두 사용하지 않더라도 존재를 알고 기본은 익혀야 합니다. 결과는 어떨까요? 제품에 대한 집중력이 떨어집니다.

인프라를 다루는 일 자체는 즐기지만, 제품을 만들어 내는 일도 그만큼 좋아합니다. 안타깝게도 많은 기업이 인프라에 소중한 시간과 비용을 낭비하며 같은 실수를 반복합니다.

인프라 고민을 아예 없앨 방법은 없을까요?

서버리스의 매력

서버리스 아키텍처는 매력적으로 보입니다. 서버도 없고 컨테이너도 없이 순수한 비즈니스 로직만 남으니까요. 하지만 나름의 과제도 안고 있습니다:

  • 콜드 스타트 시간
  • Lambda 용량 제한
  • 메모리 문제
  • 장시간 실행 프로세스 처리
  • 디버깅의 복잡성
  • 로컬 테스트의 부재

서버리스는 특정 사용 사례에서는 분명 장점이 있지만, 규모가 큰 애플리케이션에서는 여전히 서버가 필요할 수 있습니다.

서비스형 플랫폼(PaaS)

Heroku나 Netlify 같은 플랫폼은 세 번째 선택지를 제시했습니다. 모든 인프라를 대신 처리해 주는 관리형 서비스입니다. 더 이상 인프라를 걱정할 필요가 없고, 코드를 푸시하기만 하면 배포가 끝납니다. 이런 솔루션의 가장 큰 장점은 특정 프로그래밍 언어 생태계와 깊게 통합되어 있다는 점입니다.

저는 Rust 개발자를 위해 만들어지고 최고의 개발자 경험을 제공하는 플랫폼을 찾고 있었습니다. Rust 생태계(serde, sqlx, axum 등)와의 깊은 통합을 원했습니다.

얼마 전 Rust 개발 워크플로를 조금 더 매끄럽게 만들 방법을 찾다가 Shuttle을 알게 되었습니다. 기존 Rust 생태계에 자연스럽게 녹아드는 도구로, 평소대로 cargo를 사용하면서 인프라 관련 무거운 작업은 덜어낼 수 있게 해줍니다. 물론 모든 문제를 해결해 주는 마법 지팡이는 아닙니다. 하지만 Shuttle에서 마음에 들었던 점은 바로 단순함입니다. 가파른 학습 곡선을 요구하는 완전히 새로운 환경에 던져지지 않습니다. 대신 Rust 코드에 집중하면 Shuttle이 뒤에서 서버 측 복잡성을 관리해 줍니다. 요컨대, 아는 것에 충실하면서 배포와 서버 관리 측면에서는 삶을 조금 더 편하게 만드는 것이 핵심입니다. 코딩 방식 자체를 혁명적으로 바꾸는 것이 아니라, 때로는 골칫거리인 백그라운드 프로세스 관리를 미묘하게 바꿔주는 셈입니다.

지금까지의 Shuttle 경험

지금까지 Shuttle로 Zerocal과 Readable이라는 두 개의 작은 Rust 서비스를 만들었습니다.

Shuttle은 Rust 코드에 아주 적은 어노테이션만 추가하면 클라우드에 배포할 수 있게 해줍니다. 프로비저닝과 배포가 보통 서비스 구축에서 가장 고통스러운 부분임을 감안하면, 개발자 경험은 거의 이상에 가깝습니다.

대신 몇 줄의 코드만 추가하면 됩니다. 직접 보십시오. 보일러플레이트는 사라지고 비즈니스 로직만 남습니다.

Zerocal - 상태 없는 캘린더 매직

Zerocal은 제가 Shuttle에 처음 배포한 프로젝트였습니다. 원리는 매우 단순하면서도 혁신적이었습니다. 캘린더 데이터를 URL에 직접 인코딩하는 방식입니다. 덕분에 이벤트 생성은 다음과 같이 간단했습니다:

curl https://zerocal.shuttleapp.rs?start=2023-11-04+20:00&duration=3h&title=Birthday&description=paaarty

이렇게 하면 캘린더에 추가할 수 있는 iCal 파일이 반환됩니다. 브라우저에서 이벤트를 만드는 모습은 다음과 같습니다:

이 프로젝트를 만들 당시 Shuttle은 아직 이런저런 문제를 수정하고 API를 조금씩 바꾸고 있던 시기였습니다. 이런 사소한 문제들에도 불구하고 경험은 좋았습니다. 단 몇 분 만에 앱을 실행할 수 있었습니다.

다음은 axum 라우트를 포함한 서비스를 시작하는 코드입니다:

#[shuttle_runtime::main]
async fn axum() -> shuttle_axum::ShuttleAxum {
    // just normal axum routes
    let router = Router::new()
        .route("/", get(calendar))
        .route("/", post(calendar));

    Ok(router.into())
}

이제 Zerocal이 개인적으로는 크게 필요하지 않아 다른 누군가가 이어받아 주기를 바라고 있습니다. GitHub나 Discord 같은 곳에서 초대장을 공유할 때 아주 유용할 수 있다고 생각합니다. Zerocal에 대해 더 알고 싶다면 자세한 소개 글을 읽어 보시기 바랍니다.

덧붙이자면 Zerocal에서 영감을 받아 다른 분이 만든 비슷한 프로젝트도 있습니다. Mahesh Sundaram님이 Deno로 작성한 kiwi입니다. 정말 멋진 결과라고 생각합니다.

내 전자책 단말기를 위한 리더 모드

Firefox 리더 뷰에 대한 애정이 특히 전자책 단말기에 최적화된, 미니멀하고 JavaScript 없는 웹 읽기 경험을 위한 Reader Mode Proxy 제작으로 이어졌습니다. 장황한 웹사이트를 방해 요소 없이 읽기 쉬운 형태로 바꾸려는 의도였습니다.

이 프로젝트는 문제를 해결하는 단순한 앱을 좋아하는 제 개인적인 취향을 깊이 반영했습니다. 몇 개의 어노테이션만으로 제 코드가 Shuttle 환경에 매끄럽게 적응했습니다. 처음에는 테스트를 위해 제 컴퓨터에서 앱을 실행할 수 있는 자체 로컬 모드를 만들어 두었지만, Shuttle 자체 로컬 모드가 충분히 잘 작동해서 더 이상 유지할 필요가 없었습니다.

앱을 개발하는 동안 몇 가지 우여곡절도 있었습니다. 서비스 다운타임 때문에 코드를 일부 손봐야 했습니다. 하지만 Shuttle이 발전하면서, 특히 네이티브 정적 파일 처리를 도입한 이후에는 제 작업 과정 중 일부가 훨씬 단순해졌습니다.

이전에는 이렇게 생겼습니다:

#[shuttle_runtime::main]
async fn axum() -> shuttle_axum::ShuttleAxum {
    let router = Router::new()
        // Previously, I needed to manually serve static files
        .route(
            "/static/Crimson.woff2",
            get(|| async {
                static_content(
                    include_bytes!("../static/fonts/Crimson.woff2",),
                    HeaderValue::from_static("text/woff2"),
                )
            }),
        )
        .route(
            "/static/JetBrainsMono.woff2",
            get(|| async {
                static_content(
                    include_bytes!("../static/fonts/JetBrainsMono.woff2",),
                    HeaderValue::from_static("font/woff2"),
                )
            }),
        )
        .fallback(readable);

    Ok(router.into())
}

이제 이렇게 바뀌었습니다:

#[shuttle_runtime::main]
async fn axum() -> shuttle_axum::ShuttleAxum {
   let router = Router::new()
        .nest_service("/static", ServeDir::new(PathBuf::from("static")))
        .fallback(readable);
    Ok(router.into())
}

이 프로젝트의 자세한 내용은 여기서 더 자세히 살펴볼 수 있습니다.

제어와 안전성

처음에는 인프라를 위해 코드에 어노테이션을 추가하면 벤더 종속이 생기지 않을까 걱정했습니다. 프로젝트에 대한 완전한 제어권을 유지하고 싶었습니다. 옮기고 싶다면요? Shuttle 매크로는 보일러플레이트를 없애주는 역할이므로, 추가했던 2개의 어노테이션만 제거하면 원래 코드로 돌아갈 수 있습니다. Shuttle 코드 자체도 오픈소스라서 직접 셀프 호스팅 인스턴스를 구축할 수도 있습니다. 물론 저는 굳이 그러고 싶지는 않습니다.

직접 구축하는 인프라의 진짜 비용

인프라는 표면적으로는 쉬워 보일 수 있지만, 유지 관리에는 다양한 복잡성과 비용이 따릅니다. 업데이트, 배포, 가용성 등 챙겨야 할 것이 많아 압도될 수 있습니다. 이런 작업에 들이는 매 시간마다 직접 비용과 기회비용이 발생합니다.

인프라는 미로와 같을 수 있으며, Rust로 작업하는 분들에게는 Shuttle이 잘 맞는 것 같습니다. 이제 Shuttle이 무엇을 할 수 있고 없는지 어느 정도 파악했으니, 곧 Shuttle에서 더 큰 프로젝트를 시도해 볼 생각입니다. Shuttle을 한번 써볼까 고민 중이라면, 필요에 맞는지 확인하기 위해 요금제를 확인해 보는 것이 좋습니다.

인프라의 진짜 비용을 명심하십시오!

앞서 언급했듯이, 비용은 단순한 서버 비용만이 아니라 그 이상입니다. 가장 큰 요인은 인프라를 유지·보수하고 디버깅하는 데 드는 인건비이며, 이는 매우 비쌉니다. 인프라를 코드로 관리한다면 인프라를 구축하는 데만도 많은 시간을 쓰고, 유지 관리에는 그보다 더 많은 시간이 들 것입니다. 오늘날 인건비를 감안하면 큰 부담이 될 수 있습니다.

취미 프로젝트라 하더라도 저에게는 그 수고를 들일 가치가 없습니다. 모든 것을 구동하는 코드보다는 기능 개발에 집중하고 싶습니다.

원문은 Matthias Endler님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.