Rust로 NGINX 확장하기(C의 대안)

NGINX | 2023년 10월 12일

비교적 짧은 역사에도 불구하고 프로그래밍 언어 Rust는 풍부하고 성숙한 생태계를 구축하며 탁월한 찬사를 받아 왔습니다. Rust와 Cargo(Rust의 빌드 시스템이자 툴체인 인터페이스 및 패키지 관리자)는 모두 이 분야에서 높이 평가받고 선호되는 기술이며, Rust는 RedMonk의 프로그래밍 언어 순위에서 꾸준히 상위 20위권을 유지하고 있습니다. 또한 Rust를 도입한 프로젝트에서는 안정성이 향상되고 보안 관련 프로그래밍 오류가 줄어드는 경우가 많습니다(예를 들어 Android 개발자들은 단계적으로 뚜렷한 개선이 이루어진 설득력 있는 사례를 소개합니다).

F5는 한동안 Rust와 Rustacean 커뮤니티를 둘러싼 이러한 발전을 기대감 속에서 지켜봐 왔습니다. 또한 이 언어에 대한 적극적인 지지, 툴체인, 그리고 향후 도입에도 주목해 왔습니다.

NGINX는 점점 더 디지털화되고 보안에 민감해지는 세상에서 개발자들이 원하는 것과 필요로 하는 것을 충족하기 위해 이제 직접 나서고 있습니다. 기쁜 마음으로 ngx-rust 프로젝트를 소개합니다. Rust 언어로 NGINX 모듈을 작성하는 새로운 방법입니다. Rustacean 여러분을 위해 준비했습니다!

NGINX와 Rust의 간략한 역사

NGINX와 NGINX의 GitHub를 꾸준히 지켜봐 온 분이라면 이번이 Rust 기반 모듈을 처음 선보이는 것이 아니라는 사실을 알고 계실 것입니다. Kubernetes가 등장한 초기와 서비스 메시의 태동기에 Rust를 중심으로 한 작업이 일부 진행되었으며, 이는 ngx-rust 프로젝트의 기반이 되었습니다.

원래 ngx-rust는 NGINX를 사용해 Istio 호환 서비스 메시 제품의 개발을 가속화하기 위한 수단으로 활용되었습니다. 초기 프로토타입이 개발된 후 이 프로젝트는 수년간 변경 없이 방치되었습니다. 그동안 많은 커뮤니티 구성원이 저장소를 포크하거나 ngx-rust에서 제공한 기존 Rust 바인딩 예제에서 영감을 얻어 프로젝트를 만들었습니다.

시간이 흘러, 당사의 F5 Distributed Cloud Bot Defense 팀은 NGINX 프록시를 보호 서비스에 통합해야 했습니다. 이를 위해 새로운 모듈을 구축해야 했습니다.

또한 개발자 경험을 개선하고 끊임없이 변화하는 고객의 요구를 충족하는 동시에 Rust 포트폴리오를 지속적으로 확장하고자 했습니다. 이에 사내 혁신 후원 제도를 활용하고 ngx-rust의 원저자와 협력하여 새롭게 개선된 Rust 바인딩 프로젝트를 개발했습니다. 오랜 공백 끝에 문서를 보강하고 커뮤니티에서 더욱 편리하게 빌드할 수 있도록 개선한 ngx-rust 크레이트의 배포를 재개했습니다.

이것이 NGINX에 의미하는 바는 무엇일까요?

모듈은 NGINX의 핵심 구성 요소로, 대부분의 기능을 구현합니다. 또한 모듈은 NGINX 사용자가 해당 기능을 맞춤 설정하고 특정 사용 사례를 지원하도록 확장할 수 있는 가장 강력한 방법입니다.

전통적으로 NGINX는 C로 작성된 모듈만 지원해 왔습니다(C로 작성된 프로젝트인 만큼 호스트 언어로 모듈 바인딩을 지원하는 것은 명확하고 손쉬운 선택이었습니다). 하지만 컴퓨터 과학과 프로그래밍 언어 이론의 발전으로 기존 패러다임이 개선되었으며, 특히 메모리 안전성과 정확성 측면에서 큰 진전이 이루어졌습니다. 이에 따라 이제 Rust와 같은 언어도 NGINX 모듈 개발에 사용할 수 있는 길이 열렸습니다.

ngx-rust 시작하기

이제 NGINX와 Rust의 역사를 어느 정도 살펴봤으니 모듈을 만들어 보겠습니다. 소스에서 직접 빌드하여 로컬에서 모듈을 개발하거나, ngx-rust 소스를 가져와 더 나은 바인딩을 만드는 데 기여할 수도 있고, 간단히 crates.io에서 크레이트를 가져올 수도 있습니다.

The ngx-rust README에는 시작하는 데 필요한 기여 가이드라인과 로컬 빌드 요구 사항이 설명되어 있습니다. 아직 초기 개발 단계이지만, 커뮤니티의 지원을 바탕으로 품질과 기능을 개선해 나가는 것이 목표입니다. 이 튜토리얼에서는 간단한 독립형 모듈을 만드는 방법에 중점을 둡니다. 더 복잡한 내용은 ngx-rust examples에서도 확인할 수 있습니다.

바인딩은 두 개의 크레이트로 구성되어 있습니다:

  • nginx-sys는 NGINX 소스 코드에서 바인딩을 생성하는 크레이트입니다. 이 파일은 NGINX 소스 코드와 종속성을 다운로드하고 bindgen 코드 자동화를 사용하여 외부 함수 인터페이스(FFI) 바인딩을 생성합니다.
  • ngx는 Rust 글루 코드와 API를 구현하고 nginx-sys를 다시 내보내는 기본 크레이트입니다. 모듈 작성자는 이러한 심볼을 통해 NGINX를 가져와 상호 작용할 수 있으며, nginx-sys가 다시 내보내지므로 이를 명시적으로 가져올 필요가 없습니다.

아래 지침에 따라 기본 골격의 워크스페이스를 초기화합니다. 먼저 작업 디렉터리를 만들고 Rust 프로젝트를 초기화하세요:

cd $YOUR_DEV_ARENA 
mkdir ngx-rust-howto 
cd ngx-rust-howto 
cargo init --lib

다음으로 Cargo.toml 파일을 열고 다음 섹션을 추가합니다:

[lib] 
crate-type = ["cdylib"] 

[dependencies] 
ngx = "0.3.0-beta"

또는 글을 읽으면서 완성된 모듈을 확인하고 싶다면 Git에서 클론할 수 있습니다:

cd $YOUR_DEV_ARENA 
git clone git@github.com:f5yacobucci/ngx-rust-howto.git

이제 첫 번째 NGINX Rust 모듈 개발을 시작할 준비가 되었습니다. 모듈의 구조와 의미 체계, 전반적인 구축 방식은 C를 사용할 때 필요한 것과 크게 다르지 않습니다. 현재는 반복적인 접근 방식을 통해 NGINX 바인딩을 제공함으로써, 바인딩을 생성하고 실제로 사용할 수 있게 하며 개발자들이 이를 활용해 창의적인 결과물을 만들 수 있도록 하는 데 주력하고 있습니다. 앞으로는 더 나은, Rust다운 개발 경험을 제공하기 위해 노력할 것입니다.

즉, 첫 번째 단계는 NGINX에 설치하여 실행하는 데 필요한 지시어, 컨텍스트 및 기타 요소와 함께 모듈을 구성하는 것입니다. 이 모듈은 HTTP 메서드에 따라 요청을 허용하거나 거부할 수 있는 간단한 핸들러이며, 단일 인수를 받는 새로운 지시어를 생성합니다. 단계별로 살펴보겠지만, 전체 코드는 GitHub의 ngx-rust-howto 리포지토리에서 확인할 수 있습니다.

참고: 이 블로그에서는 일반적인 NGINX 모듈 빌드 방법보다는 Rust 관련 세부 사항을 중점적으로 설명합니다. 다른 NGINX 모듈을 빌드하는 데 관심이 있다면 커뮤니티에 공개된 여러 훌륭한 자료를 참고하세요. 이러한 자료에서는 NGINX를 확장하는 방법의 기본 원리도 더욱 자세히 설명합니다(자세한 내용은 아래의 리소스 섹션 참조).

모듈 등록

다음을 구현하여 Rust 모듈을 만들 수 있습니다: HTTPModule 트레이트. 이 트레이트는 모든 NGINX 진입점(postconfiguration, preconfiguration, create_main_conf 등)을 정의합니다. 모듈 작성자는 작업에 필요한 함수만 구현하면 됩니다. 이 모듈에서는 요청 핸들러를 설치하기 위해 postconfiguration 메서드를 구현합니다.

참고: ngx-rust-howto 저장소를 복제하지 않았다면 src/lib.rs 명령으로 생성된 파일을 편집하면 됩니다: cargo init.

struct Module; 

impl http::HTTPModule for Module { 
    type MainConf = (); 
    type SrvConf = (); 
    type LocConf = ModuleConfig; 

    unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t { 
        let htcf = http::ngx_http_conf_get_module_main_conf(cf, &ngx_http_core_module); 

        let h = ngx_array_push( 
            &mut (*htcf).phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers, 
        ) as *mut ngx_http_handler_pt; 
        if h.is_null() { 
            return core::Status::NGX_ERROR.into(); 
        } 

        // set an Access phase handler 
        *h = Some(howto_access_handler); 
        core::Status::NGX_OK.into() 
    } 
} 

Rust 모듈에는 액세스 단계인 postconfiguration에서 NGX_HTTP_ACCESS_PHASE 후크만 있으면 됩니다. 모듈은 HTTP 요청의 여러 단계에 대한 핸들러를 등록할 수 있습니다. 자세한 내용은 개발 가이드를 참조하세요.

함수가 반환되기 직전에 단계 핸들러 howto_access_handler가 추가된 것을 볼 수 있습니다. 이에 대해서는 나중에 다시 살펴보겠습니다. 지금은 이 함수가 요청 처리 체인에서 처리 로직을 수행한다는 점만 알아두세요.

모듈 유형과 요구 사항에 따라 사용할 수 있는 등록 훅은 다음과 같습니다:

  • 사전 구성
  • postconfiguration
  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

구성 상태

이제 모듈을 위한 저장소를 만들 차례입니다. 이 데이터에는 필요한 구성 매개변수나 요청을 처리하거나 동작을 변경하는 데 사용되는 내부 상태가 포함됩니다. 즉, 모듈에서 유지해야 하는 모든 정보를 구조체에 넣어 저장할 수 있습니다. 이 Rust 모듈은 location 구성 수준에서 ModuleConfig 구조체를 사용합니다. 구성 저장소는 Merge 및 Default 트레이트를 구현해야 합니다.

위 단계에서 모듈을 정의할 때 기본, 서버 및 위치 구성의 유형을 설정할 수 있습니다. 여기서 개발하는 Rust 모듈은 위치 구성만 지원하므로 LocConf 유형만 설정합니다.

모듈의 상태 및 구성을 저장할 공간을 만들려면 구조체를 정의하고 Merge 트레이트를 구현하세요:

#[derive(Debug, Default)] 
struct ModuleConfig { 
    enabled: bool, 
    method: String, 
} 

impl http::Merge for ModuleConfig { 
    fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> { 
        if prev.enabled { 
            self.enabled = true; 
        } 

        if self.method.is_empty() { 
            self.method = String::from(if !prev.method.is_empty() { 
                &prev.method 
            } else { 
                "" 
            }); 
        } 

        if self.enabled && self.method.is_empty() { 
            return Err(MergeConfigError::NoValue); 
        } 
        Ok(()) 
    } 
} 

ModuleConfig는 enabled 필드에 활성화/비활성화 상태와 HTTP 요청 메서드를 저장합니다. 핸들러는 이 메서드를 확인하여 요청을 허용하거나 차단합니다.

저장소가 정의되면 모듈에서 사용자가 직접 설정할 수 있는 지시어와 구성 규칙을 만들 수 있습니다. NGINX는 ngx_command_t 형식과 배열을 사용하여 모듈에서 정의한 지시어를 코어 시스템에 등록합니다.

FFI 바인딩을 통해 Rust 모듈 작성자는 ngx_command_t 타입을 사용할 수 있으며 C에서와 같은 방식으로 지시어를 등록할 수 있습니다. ngx-rust-howto 모듈은 문자열 값을 받는 howto 지시어를 정의합니다. 이 경우 명령 하나를 정의하고 setter 함수를 구현한 다음, 다음 섹션에서 해당 명령을 코어 시스템에 연결합니다. 제공된 ngx_command_null! 매크로로 명령 배열을 끝내야 합니다.

NGINX 명령을 사용하여 간단한 지시어를 만드는 방법은 다음과 같습니다:

#[no_mangle] 
static mut ngx_http_howto_commands: [ngx_command_t; 2] = [ 
    ngx_command_t { 
        name: ngx_string!("howto"), 
        type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, 
        set: Some(ngx_http_howto_commands_set_method), 
        conf: NGX_RS_HTTP_LOC_CONF_OFFSET, 
        offset: 0, 
        post: std::ptr::null_mut(), 
    }, 
    ngx_null_command!(), 
]; 

#[no_mangle] 
extern "C" fn ngx_http_howto_commands_set_method( 
    cf: *mut ngx_conf_t, 
    _cmd: *mut ngx_command_t, 
    conf: *mut c_void, 
) -> *mut c_char { 
    unsafe { 
        let conf = &mut *(conf as *mut ModuleConfig); 
        let args = (*(*cf).args).elts as *mut ngx_str_t; 
        conf.enabled = true; 
        conf.method = (*args.add(1)).to_string(); 
    }; 

    std::ptr::null_mut() 
} 

모듈 연결하기

이제 등록 함수, 단계 핸들러, 구성용 명령이 준비되었으므로, 이들을 모두 연결하고 해당 함수들을 코어 시스템에 노출할 수 있습니다. 등록 함수, 단계 핸들러 및 지시어 명령에 대한 참조를 포함하는 정적 ngx_module_t 구조체를 만드세요. 모든 모듈에는 ngx_module_t 타입의 전역 변수가 있어야 합니다.

그런 다음 컨텍스트와 정적 모듈 타입을 생성하고, ngx_modules! 매크로를 사용해 이를 노출합니다. 아래 예제에서는 commands가 commands 필드에 설정되고, 모듈 등록 함수를 참조하는 컨텍스트가 ctx 필드에 설정되는 방식을 확인할 수 있습니다. 이 모듈의 다른 모든 필드는 사실상 기본값을 사용합니다.

#[no_mangle] 
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { 
    preconfiguration: Some(Module::preconfiguration), 
    postconfiguration: Some(Module::postconfiguration), 
    create_main_conf: Some(Module::create_main_conf), 
    init_main_conf: Some(Module::init_main_conf), 
    create_srv_conf: Some(Module::create_srv_conf), 
    merge_srv_conf: Some(Module::merge_srv_conf), 
    create_loc_conf: Some(Module::create_loc_conf), 
    merge_loc_conf: Some(Module::merge_loc_conf), 
}; 

ngx_modules!(ngx_http_howto_module); 

#[no_mangle] 
pub static mut ngx_http_howto_module: ngx_module_t = ngx_module_t { 
    ctx_index: ngx_uint_t::max_value(), 
    index: ngx_uint_t::max_value(), 
    name: std::ptr::null_mut(), 
    spare0: 0, 
    spare1: 0, 
    version: nginx_version as ngx_uint_t, 
    signature: NGX_RS_MODULE_SIGNATURE.as_ptr() as *const c_char, 

    ctx: &ngx_http_howto_module_ctx as *const _ as *mut _, 
    commands: unsafe { &ngx_http_howto_commands[0] as *const _ as *mut _ }, 
    type_: NGX_HTTP_MODULE as ngx_uint_t, 

    init_master: None, 
    init_module: None, 
    init_process: None, 
    init_thread: None, 
    exit_thread: None, 
    exit_process: None, 
    exit_master: None, 

    spare_hook0: 0, 
    spare_hook1: 0, 
    spare_hook2: 0, 
    spare_hook3: 0, 
    spare_hook4: 0, 
    spare_hook5: 0, 
    spare_hook6: 0, 
    spare_hook7: 0, 
}; 

이제 새 Rust 모듈을 설정하고 등록하는 데 필요한 단계는 거의 완료되었습니다. 다만 (howto_access_handler) that was set in the postconfiguration 훅에서 설정한 단계 핸들러는 아직 구현해야 합니다.

핸들러

핸들러는 들어오는 각 요청마다 호출되며 모듈 작업의 대부분을 수행합니다. 요청 핸들러는 ngx-rust 팀이 중점적으로 개발해 온 부분으로, 초기 사용 편의성 개선의 대부분이 여기에 적용되었습니다. 앞선 설정 단계에서는 C와 유사한 스타일로 Rust 코드를 작성해야 하지만, ngx-rust는 요청 핸들러를 위한 더욱 편리한 기능과 유틸리티를 제공합니다.

아래 예제에서 볼 수 있듯이 ngx-rust는 http_request_handler! 매크로를 제공하여 Request 인스턴스와 함께 호출되는 Rust 클로저를 받을 수 있습니다. 또한 구성과 변수를 가져오고, 해당 변수를 설정하며, 메모리와 기타 NGINX 기본 요소 및 API에 액세스할 수 있는 유틸리티도 제공합니다.

핸들러 프로시저를 시작하려면 매크로를 호출하고 비즈니스 로직을 Rust 클로저로 제공합니다. ngx-rust-howto 모듈에서는 요청 처리가 계속되도록 요청의 메서드를 확인합니다.

http_request_handler!(howto_access_handler, |request: &mut http::Request| { 
    let co = unsafe { request.get_module_loc_conf::(&ngx_http_howto_module) }; let co = co.expect("module config is none"); ngx_log_debug_http!(request, "howto module enabled called"); match co.enabled { true => { let method = request.method(); if method.as_str() == co.method { return core::Status::NGX_OK; } http::HTTPStatus::FORBIDDEN.into() } false => core::Status::NGX_OK, } }); 

이것으로 첫 번째 Rust 모듈을 완성했습니다!

GitHub의 ngx-rust-howto 리포지토리에는 conf 디렉터리에 NGINX 구성 파일이 있습니다. 또한 cargo build로 빌드하고 모듈 바이너리를 로컬 nginx.conf의 load_module 지시문에 추가한 다음, NGINX 인스턴스를 사용하여 실행할 수도 있습니다. 이 튜토리얼에서는 ngx-rust가 기본적으로 지원하는 NGINX_VERSION인 NGINX v1.23.3을 사용했습니다. 동적 모듈을 빌드하고 실행할 때는 ngx-rust 빌드에 사용하는 NGINX_VERSION이 컴퓨터에서 실행 중인 NGINX 인스턴스의 버전과 동일한지 확인하세요.

결론

NGINX는 오랜 기간에 걸쳐 다양한 기능과 사용 사례가 축적된 성숙한 소프트웨어 시스템입니다. 뛰어난 프록시이자 로드 밸런서이며, 세계적인 수준의 웹 서버이기도 합니다. 앞으로도 오랫동안 시장에서 확고한 입지를 유지할 것이기에, 저희는 NGINX의 기능을 더욱 발전시키고 사용자가 NGINX와 상호 작용할 수 있는 새로운 방법을 제공하고자 합니다. 개발자들 사이에서 Rust의 인기가 높아지고 향상된 안전성 제약 조건이 주목받는 가운데, 세계 최고의 웹 서버와 함께 Rust를 사용할 수 있는 선택지를 제공하게 되어 기쁩니다.

하지만 NGINX의 높은 성숙도와 풍부한 기능을 갖춘 생태계로 인해 API의 범위가 방대하며, ngx-rust는 아직 그 가능성의 일부만 다루고 있습니다. 이 프로젝트는 보다 Rust다운 인터페이스를 추가하고, 참조 모듈을 더 많이 구축하며, 모듈 작성 편의성을 향상함으로써 지속적으로 개선하고 확장하는 것을 목표로 합니다.

바로 여러분이 참여할 차례입니다! ngx-rust 프로젝트는 누구에게나 열려 있으며 GitHub에서 확인할 수 있습니다. NGINX 커뮤니티와 협력하여 모듈의 기능과 사용 편의성을 지속적으로 개선해 나가고자 합니다. 직접 살펴보고 바인딩을 사용해 보세요! 또한 언제든지 문의하거나 이슈 또는 PR을 제출하고, NGINX 커뮤니티 Slack 채널에서 저희와 소통해 주세요.

리소스


공유
태그: F5 NGINX, Tech

관련 블로그 게시물

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
NGINX | 2023. 11. 30.

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric

이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
NGINX | 2023. 03. 23.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법

앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
NGINX | 2021. 09. 27.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션

개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX | 2021. 07. 06.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법

NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX | 2021. 06. 30.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리

NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
NGINX | 2015. 03. 25.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유

Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.