ブログ | NGINX

Rust で NGINX を拡張する (C の代替)

マシュー・ヤコブッチ サムネイル
マシュー・ヤコブッチ
2023年10月12日公開

プログラミング言語Rust は、比較的短い歴史の中で、豊かで成熟したエコシステムとともに、並外れた称賛を獲得してきました。 Rust と Cargo (そのビルド システム、ツールチェーン インターフェイス、パッケージ マネージャー) はどちらも、業界で高く評価され、望まれているテクノロジーであり、Rust はRedMonk のプログラミング言語ランキングの上位 20 言語で安定した位置を占めています。 さらに、Rust を採用したプロジェクトでは、安定性とセキュリティ関連のプログラミング エラーが改善されることがよくあります (例として、Android 開発者は、目覚ましい改善の説得力のあるストーリーを語っています)。

F5 は、Rust とその Rustaceans コミュニティをめぐるこうした進展を、しばらくの間、興奮しながら見守ってきました。 私たちは、この言語、そのツールチェーン、そして今後の採用を積極的に推進することで注目してきました。

NGINX では、ますますデジタル化が進み、セキュリティ意識が高まる世界において、開発者の要望とニーズを満たすために積極的に取り組んでいます。 Rust 言語を使用して NGINX モジュールを記述する新しい方法であるngx-rust プロジェクトを発表できることを嬉しく思います。 Rustaceans さん、これはあなたのためのものです!

NGINX と Rust の簡単な歴史

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からクレートをプルしたりすることができます。

ngx-rust READMEには、貢献ガイドラインと開始するためのローカル ビルド要件が記載されています。 まだ初期段階であり、開発中ですが、コミュニティのサポートを受けて品質と機能を向上させることを目指しています。 このチュートリアルでは、単純な独立したモジュールの作成に焦点を当てます。 より複雑なレッスンについては、 ngx-rust の例を参照することもできます。

バインディングは 2 つのクレートに編成されます。

  • 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 を拡張する方法についてもより基本的な説明が提供されます (詳細については、以下のリソースセクションを参照してください)。

モジュール登録

すべての NGINX エントリ ポイント ( postconfigurationpreconfigurationcreate_main_confなど) を定義するHTTPModule特性を実装することで、Rust モジュールを作成できます。 モジュール作成者は、そのタスクに必要な関数を実装するだけで済みます。 このモジュールは、リクエスト ハンドラーをインストールするための postconfiguration メソッドを実装します。

注記: 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でのpostconfigurationフックのみが必要です。 モジュールは、HTTP リクエストのさまざまなフェーズのハンドラーを登録できます。 詳細については、開発ガイドの詳細を参照してください。

関数が返される直前に、フェーズ ハンドラーhowto_access_handlerが追加されていることがわかります。 これについては後でまた取り上げます。 現時点では、リクエスト チェーン中に処理ロジックを実行する関数であることに注意してください。

モジュールの種類とそのニーズに応じて、次の登録フックが利用できます。

  • 事前設定
  • 構成後
  • メイン設定を作成します
  • 初期化メイン
  • 作成_srv_conf
  • マージ_srv_conf
  • 作成場所設定
  • マージ_loc_conf

構成状態

次に、モジュール用のストレージを作成します。 このデータには、必要な構成パラメータや、リクエストの処理や動作の変更に使用される内部状態が含まれます。 基本的に、モジュールが保持する必要がある情報はすべて構造体に入れて保存できます。 この Rust モジュールは、場所構成レベルで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 は、HTTP リクエスト メソッドとともに、有効フィールドにオン/オフ状態を保存します。 ハンドラーはこのメソッドをチェックし、リクエストを許可または禁止します。

ストレージが定義されると、モジュールはユーザーが自分で設定できるディレクティブと構成ルールを作成できます。 NGINX は、 ngx_command_t型と配列を使用して、モジュール定義のディレクティブをコア システムに登録します。

FFI バインディングを通じて、Rust モジュール作成者はngx_command_t 型にアクセスし、C の場合と同じようにディレクティブを登録できます。ngx-rust-howtoモジュールは、文字列値を受け入れるhowtoディレクティブを定義します。 この場合、1 つのコマンドを定義し、セッター関数を実装し、(次のセクションで) それらのコマンドをコア システムにフックします。 提供されている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!マクロで公開します。 以下の例では、コマンドがコマンド フィールドにどのように設定され、モジュール登録関数を参照するコンテキストが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 モジュールを設定して登録するために必要な手順が実質的に完了しました。 ただし、 postconfigurationフックで設定されたフェーズ ハンドラー(howto_access_handler)を実装する必要があります。

ハンドラー

ハンドラは受信リクエストごとに呼び出され、モジュールの作業の大部分を実行します。 リクエスト ハンドラーは ngx-rust チームの焦点であり、初期の人間工学的改善の大部分がここで行われました。 これまでのセットアップ手順では Rust を C のようなスタイルで記述する必要がありましたが、ngx-rust はリクエスト ハンドラーにさらなる利便性とユーティリティを提供します。

以下の例に示すように、ngx-rust は、 Requestインスタンスで呼び出される 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 インスタンスと同じNGINX_VERSION をngx-rust ビルドに使用するようにしてください。

結論

NGINX は、長年にわたる機能とユースケースが組み込まれた成熟したソフトウェア システムです。 これは、優れたプロキシ、ロードバランサ、そして世界クラスの Web サーバーです。 今後何年もの間、この製品が市場に存在することは確実であり、それが、この製品の機能をさらに強化し、ユーザーに新しい方法で製品と対話できるようにするという当社の意欲を高めています。 Rust は開発者の間で人気があり、安全性の制約も改善されているため、世界最高の Web サーバーと併せて Rust を使用するオプションを提供できることを嬉しく思います。

ただし、NGINX の成熟度と機能豊富なエコシステムにより、API の表面領域が拡大しており、ngx-rust はまだその表面に触れたにすぎません。 このプロジェクトは、より慣用的な Rust インターフェースの追加、追加のリファレンス モジュールの構築、モジュール作成の人間工学の向上を通じて、改善と拡張を目指しています。

ここであなたの出番です! ngx-rust プロジェクトは誰でも参加でき、 GitHub で入手できます。 私たちは、NGINX コミュニティと協力して、モジュールの機能と使いやすさを継続的に改善していきたいと考えています。 ぜひチェックして、バインディングを自分で試してみてください。 ぜひ、NGINX コミュニティ Slack チャネルにご連絡いただき、問題や PR を提出し、ご参加ください。

リソース


「このブログ投稿には、入手できなくなった製品やサポートされなくなった製品が参照されている場合があります。 利用可能な F5 NGINX 製品およびソリューションに関する最新情報については、 NGINX 製品ファミリーをご覧ください。 NGINX は現在 F5 の一部です。 q。"