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

NGINX에서는 점점 더 디지털화되고 보안에 민감한 세상에서 개발자의 요구와 필요를 충족하기 위해 이제 게임에 스킨을 적용하고 있습니다. Rust 언어로 NGINX 모듈을 작성하는 새로운 방법인 ngx-rust 프로젝트를 발표하게 되어 기쁘게 생각합니다. 이것은 여러분을 위한 것입니다.

비교적 짧은 역사에도 불구하고 프로그래밍 언어 Rust는 풍부하고 성숙한 에코시스템과 함께 뛰어난 찬사를 받았습니다. Rust 와 Cargo(빌드 시스템, 툴 체인 인터페이스, 패키지 관리자)는 모두 업계에서 존경받고 선호되는 기술이며, RedMonk의 프로그래밍 언어 순위에서 상위 20개 언어 중 안정적인 순위를 차지하고 있습니다. 또한 Rust를 채택한 프로젝트에서는 안정성 및 보안 관련 프로그래밍 오류가 개선되는 경우가 많습니다(예를 들어 Android 개발자들은 눈에 띄게 개선된 사례에 대해 설득력 있는 이야기를 들려줍니다).

F5는 한동안 Rust와 Rust 커뮤니티의 이러한 발전을 흥미롭게 지켜봐 왔습니다. 언어와 도구 체인, 그리고 앞으로의 채택에 대해 적극적으로 지지하며 주목해 왔습니다.

목차

1. NGINX와 Rust의 간략한 역사
2. 이는 NGINX에 어떤 의미인가요?
3. ngx-rust를 시작하는 방법
3-1. 모듈 등록
3-2. 구성 상태
3-3. 모듈에 연결하기
3-4. 핸들러
4. 결론

1. NGINX와 Rust 의 간략한 역사

NGINX와 NGINX GitHub의 팔로워라면 이것이 Rust 기반 모듈의 첫 번째 화신이 아니라는 것을 알고 계실 것입니다. Kubernetes 초창기와 Service Mesh 초창기에는 Rust를 중심으로 일부 작업이 나타나면서 ngx-rust 프로젝트의 토대가 만들어졌습니다.

원래 ngx-rust는 NGINX와 호환되는 Service Mesh 제품의 개발을 가속화하는 방법으로 사용되었습니다. 초기 프로토타입을 개발한 후 이 프로젝트는 수년 동안 변경되지 않은 채 방치되었습니다. 그 기간 동안 많은 커뮤니티 구성원이 리포지토리를 포크하거나 ngx-rust에서 제공된 오리지널 Rust 바인딩 예제에서 영감을 받아 프로젝트를 만들었습니다.

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

또한 개발자 경험을 개선하고 고객의 진화하는 요구 사항을 충족하면서 Rust 포트폴리오를 계속 확장하기를 원했습니다. 그래서 내부 혁신 후원을 활용하고 원래 ngx-rust 작성자와 협력하여 새롭고 개선된 Rust 바인딩 프로젝트를 개발했습니다. 오랜 중단 끝에 커뮤니티 사용을 위한 인체공학적 설계를 구축하기 위해 향상된 문서와 개선 사항으로 ngx-rust 크레이트 게시를 다시 시작했습니다.

2. Rust 는 NGINX에 어떤 의미인가요?

모듈은 대부분의 기능을 구현하는 NGINX의 핵심 빌딩 블록입니다. 또한 모듈은 NGINX 사용자가 해당 기능을 사용자 정의하고 특정 사용 사례에 대한 지원을 구축할 수 있는 가장 강력한 방법이기도 합니다.

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

3. ngx-rust 를 시작하는 방법

이제 NGINX와 Rust의 역사에 대해 알아보았으니 모듈 빌드를 시작해 보겠습니다. 소스에서 빌드하여 로컬에서 개발하거나, ngx-rust 소스를 가져와 더 나은 바인딩을 빌드하거나, 단순히 creates.io에서 크레이트를 가져올 수 있습니다.

시작하기 위한 기여 가이드라인과 로컬 빌드 요구 사항은 ngx-rust README에서 확인할 수 있습니다. 아직 초기 개발 단계이지만 커뮤니티의 지원을 받아 품질과 기능을 개선하는 것을 목표로 하고 있습니다. 이 튜토리얼에서는 간단한 독립 모듈을 만드는 데 중점을 둡니다. 좀 더 복잡한 예제를 보려면 ngx-rust 예제를 참조하세요.

바인딩은 두 개의 크레이트로 구성됩니다

  • nginx-sys는 NGINX 소스 코드에서 바인딩을 생성하는 크레이트입니다. 이 파일은 NGINX 소스 코드, 종속성을 다운로드하고 bindgen 코드 자동화를 사용하여 외부 함수 인터페이스(FFI) 바인딩을 생성합니다.
  • ngx는 Rust glue 코드, 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를 확장하는 방법에 대한 보다 근본적인 설명도 확인할 수 있습니다.

3-1. 모듈 등록

모든 NGINX 진입점(사후 구성, 사전 구성, create_main_conf 등)을 정의하는 HTTPModule 특성을 구현하여 Rust 모듈을 생성할 수 있습니다. 모듈 작성자는 해당 작업에 필요한 함수만 구현하면 됩니다. 이 모듈은 요청 핸들러를 설치하기 위해 사후 구성 메서드를 구현합니다.

참고: ngx-rust-howto 리포지토리를 복제하지 않은 경우 cargo init에서 생성한 src/lib.rs 파일 편집을 시작할 수 있습니다.

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 모듈은 액세스 단계인 NGX_HTTP_ACCESS_PHASE에서만 사후 구성 후크가 필요합니다. 모듈은 HTTP 요청의 다양한 단계에 대한 핸들러를 등록할 수 있습니다. 이에 대한 자세한 내용은 개발 가이드의 세부 정보를 참조하세요.

함수가 반환되기 직전에 단계 핸들러 howto_access_handler가 추가된 것을 볼 수 있습니다. 이 부분은 나중에 다시 설명하겠습니다. 지금은 요청 체인 중에 처리 로직을 수행하는 함수라는 점만 기억하세요.

모듈 유형과 필요에 따라 사용 가능한 등록 후크는 다음과 같습니다:

  • preconfiguration
  • postconfiguration
  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

3-2. 구성 상태

이제 모듈을 위한 스토리지를 만들 차례입니다. 이 데이터에는 필요한 구성 매개변수나 요청을 처리하거나 동작을 변경하는 데 사용되는 내부 상태가 포함됩니다. 기본적으로 모듈이 유지해야 하는 정보는 무엇이든 구조체에 넣고 저장할 수 있습니다. 이 Rust 모듈은 location 구성 수준에서 ModuleConfig 구조를 사용합니다. 구성 저장소는 병합 및 기본 특성을 구현해야 합니다.

위 단계에서 모듈을 정의할 때 main, server 및 location 구성에 대한 유형을 설정할 수 있습니다. 여기서 개발 중인 Rust 모듈은 location만 지원하므로 LocConf 유형만 설정됩니다.

모듈에 대한 상태 및 구성 저장소를 만들려면 구조를 정의하고 병합을 구현하세요.

trait:#[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는 HTTP 요청 메서드와 함께 활성화 필드에 on/off 상태를 저장합니다. 핸들러는 이 메서드와 비교하여 요청을 허용하거나 금지합니다.

저장소가 정의되면 모듈은 사용자가 직접 설정할 수 있는 지시문과 구성 규칙을 생성할 수 있습니다. NGINX는 ngx_command_t 타입과 배열을 사용하여 모듈 정의 지시문을 코어 시스템에 등록합니다.

Rust 모듈 작성자는 FFI 바인딩을 통해 ngx_command_t 유형에 액세스하고 C에서와 마찬가지로 지시문을 등록할 수 있습니다. ngx-rust-howto 모듈은 문자열 값을 허용하는 howto 지시문을 정의합니다. 이 경우 하나의 명령을 정의하고, 세터 함수를 구현한 다음 (다음 섹션에서) 해당 명령을 코어 시스템에 연결합니다. 제공된 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()
}

3-3. 모듈에 연결하기

이제 등록 함수, 페이즈 핸들러, 구성용 명령어가 준비되었으므로 모든 것을 서로 연결하고 함수를 코어 시스템에 노출할 수 있습니다. 등록 함수, 페이즈 핸들러 및 지시문 명령에 대한 참조가 포함된 정적 ngx_module_t 구조를 만듭니다. 모든 모듈에는 ngx_module_t 유형의 전역 변수가 포함되어야 합니다.

그런 다음 컨텍스트 및 정적 모듈 유형을 생성하고 ngx_module! 매크로를 사용하여 노출합니다. 아래 예제에서는 명령이 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)를 구현해야 합니다.

3-4. 핸들러

핸들러는 들어오는 각 요청에 대해 호출되며 모듈의 대부분의 작업을 수행합니다. 요청 핸들러는 ngx-rust 팀이 중점을 두고 있으며, 초기 인체공학적 개선의 대부분이 이루어진 곳이기도 합니다. 이전 설정 단계에서는 rust를 C와 같은 스타일로 작성해야 했지만, ngx-rust는 요청 핸들러에 더 많은 편의성과 유틸리티를 제공합니다.

아래 예제에서 볼 수 있듯이, ngx-rust는 요청 인스턴스와 함께 호출된 Rust 클로저를 수락하기 위한 매크로 http_request_handler! 를 제공합니다. 또한 구성 및 변수를 가져오고, 해당 변수를 설정하고, 메모리, 기타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을 사용했습니다. 동적 모듈을 빌드하고 실행할 때는 머신에서 실행중인 NGINX 인스턴스와 동일한 ngx-rust 빌드에 동일한 NGINX_VERSION을 사용해야 합니다.

4. 결론

NGINX는 수년간의 기능과 사용 사례가 내장된 성숙한 소프트웨어 시스템입니다. 유능한 프록시, 로드 밸런서 이자 세계적 수준의 웹 서버입니다. 앞으로 몇 년 동안 시장에서 그 존재감이 확실하며, 이는 그 기능을 기반으로 사용자에게 새로운 상호 작용 방법을 제공하고자 하는 동기를 부여합니다. 개발자들 사이에서 Rust의 인기와 개선된 안전 제약 조건으로 인해 세계 최고의 웹 서버와 함께 Rust를 사용할 수 있는 옵션을 제공하게 되어 매우 기쁩니다.

그러나 NGINX의 성숙도와 풍부한 기능의 에코시스템으로 인해 API 표면적이 넓어졌고, ngx-rust는 이제 시작에 불과합니다. 이 프로젝트는 더 많은 관용적인 Rust 인터페이스를 추가하고, 추가 참조 모듈을 구축하고, 모듈 작성의 인체공학을 발전시킴으로써 개선하고 확장하는 것을 목표로 합니다.

지금 바로 여러분이 참여하세요! ngx-rust 프로젝트는 누구에게나 열려 있으며 GitHub에서 사용할 수 있습니다. 모듈의 기능과 사용 편의성을 계속 개선하기 위해 NGINX 커뮤니티와 협력하고 싶습니다. 직접 확인하고 바인딩을 실험해 보세요! 그리고 NGINX 커뮤니티 Slack 채널에 연락하여 문제나 PR을 제출하고 소통해 주세요.

NGINX Plus를 직접 사용해 보시려면 30일 무료 평가판을 신청하거나 NGINX STORE에 연락하여 문의하십시오.

NGINX에 대한 최신 정보들을 빠르게 전달받고 싶으시다면, 아래의 뉴스레터를 구독하세요.

NGINX STORE를 통한 솔루션 도입 및 기술지원 무료 상담 신청

* indicates required