RustによるNGINXの拡張(Cに代わる選択肢)

NGINX | 2023年10月12日

プログラミング言語の「Rust」は、比較的短い歴史の中で非常に高い評価を得るとともに、充実した成熟度の高いエコシステムを築いてきました。RustとCargo(Rustのビルドシステム、ツールチェーンインターフェース、パッケージマネージャー)は、現在の技術環境において高く評価され、利用が望まれているテクノロジーです。また、RustはRedMonkのプログラミング言語ランキングで安定して上位20言語に入っています。さらに、Rustを採用したプロジェクトでは、安定性が向上し、セキュリティ関連のプログラミングエラーが減少する傾向があります(たとえば、Android開発者は、段階的に大きな改善を遂げた説得力のある事例を紹介しています)。

F5は以前から、Rustをめぐるこうした進展とRustaceanコミュニティに大きな期待を寄せ、注目してきました。そして、Rust言語を積極的に支持するとともに、そのツールチェーンと今後の導入も推進しています。

デジタル化が進み、セキュリティへの意識が高まる世界で、開発者の要望やニーズに応えるため、NGINXも本格的に取り組みます。このたび、ngx-rustプロジェクトを発表します。Rust言語でNGINXモジュールを記述する新たな方法です。Rustaceanの皆さまのためのプロジェクトです!

NGINXとRustの略史

NGINXとF5の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からcrateを取得するだけでもかまいません。

The ngx-rust READMEには、開発を始めるためのコントリビューションガイドラインとローカルビルドの要件が記載されています。まだ初期開発段階ですが、コミュニティの支援を得ながら品質と機能の向上を目指しています。このチュートリアルでは、シンプルな独立モジュールの作成に焦点を当てます。より複雑な内容については、ngx-rust examplesも参照してください。

バインディングは、次の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を拡張する方法について、より基礎的な説明も得られます(詳しくは、以下の「リソース」セクションをご覧ください)。

モジュールの登録

You can create your Rust module by implementing the HTTPModuleトレイトを実装することで、Rustモジュールを作成できます。このトレイトでは、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が追加されていることがわかります。これについては後ほど改めて説明します。ここでは、リクエストチェーン内で処理ロジックを実行する関数であることだけ覚えておいてください。

モジュールの種類と要件に応じて、次の登録フックを使用できます。

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

構成状態

次に、モジュール用のストレージを作成します。このデータには、必要な構成パラメーターや、リクエストの処理または動作の変更に使用する内部状態が含まれます。つまり、モジュールで永続化する必要がある情報は、構造体に格納して保存できます。このRustモジュールでは、location構成レベルでModuleConfig構造体を使用します。構成ストレージには、Merge and Defaultトレイトを実装する必要があります。

上記の手順でモジュールを定義する際に、main、server、locationの各設定の型を指定できます。ここで開発するRustモジュールはlocationのみをサポートするため、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ディレクティブを定義します。この例では、コマンドを1つ定義して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インスタンスと同じNGINX_VERSIONを必ず使用してください。

まとめ

NGINXは、長年にわたって機能とユースケースが蓄積されてきた成熟したソフトウェアシステムです。高性能なプロキシおよびロードバランサーであり、世界トップクラスのWebサーバーでもあります。今後も長年にわたり市場で確固たる地位を維持すると見込まれることが、その機能をさらに発展させ、ユーザーがNGINXを活用するための新たな方法を提供する原動力となっています。開発者の間でRustの人気が高まり、安全性に関する制約も強化されていることから、世界最高峰のWebサーバーとともにRustを使用できる選択肢を提供できることをうれしく思います。

しかし、成熟したNGINXと機能豊富なエコシステムにより、APIの対象範囲は広大であり、ngx-rustが対応できているのはまだごく一部にすぎません。このプロジェクトでは、よりRustらしいインターフェースの追加、リファレンスモジュールの拡充、モジュール開発の使いやすさの向上を通じて、機能の改善と拡張を目指しています。

ここからは皆さまの出番です。ngx-rustプロジェクトには誰でも参加でき、GitHubで公開されています。NGINコミュニティの皆さまと協力し、モジュールの機能と使いやすさを継続的に向上させていきたいと考えています。ぜひご覧いただき、実際にバインディングをお試しください。また、お気軽にお問い合わせいただくほか、IssueやPRを作成したり、NGIN Community Slackチャンネルで交流したりしてください。

リソース


共有
タグ: F5 NGINX, Tech

関連するブログ記事

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
NGINX | 2024/01/18

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する

OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
NGINX | 2023/05/31

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正

Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
NGINX | 2023/02/09

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法

Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINX | 2022/05/24

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング

NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
NGINX | 2018/08/17

SELinuxでNGINXおよびNGINX Plusを使用する

Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。

NGINX PlusによるAWS Auto Scalingグループの負荷分散
NGINX | 2017/03/06

NGINX PlusによるAWS Auto Scalingグループの負荷分散

新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります