Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rust x Web x GCP をやっていく

110416
September 29, 2023

Rust x Web x GCP をやっていく

Talk@2023/09/29 人工衛星の開発現場でLT大会 〜Rust バックエンド開発特集〜

Rust でバックエンド開発、特に GCP 上でサーバーを動かす際に使えるエコシステムを紹介する.

110416

September 29, 2023
Tweet

More Decks by 110416

Other Decks in Technology

Transcript

  1. Rust@Work Rust はモバイルアプリの BFF で主に利用しています. BFF は gRPC でクライアントと通信し ます.

    サービスは Cloud Run や社内の GKE クラスタにデプロイしています. また、開発者向けの CLI、マークアップパーサーや AST を Rust で実装しています.
  2. tower について: tower tower はもっとも抽象的なレイヤー. 型でいうと  async fn(Request) -> Result<Response,

    Error> . Request 型の入力を受け取って、非同期の文脈で Response 型または Error 型の出力を 返す関数を一般化したもの. Request ・ Response ・ Error は HTTP に限らないジェネリックな型. Layer: Request -> Request , Response -> Response によって機能を plug-in できる.
  3. hyper について hyper は HTTP の関心ごとを扱う低レベル(プリミティブ)なライブラリ. Rust のウェブフレームワーク Axum や

    Warp、HTTP クライアントの reqwest は hyper をベ ースに実装されている. hyper is a relatively low-level library, meant to be a building block for libraries and applications. 古い hyper は tower サービスを実装していたが新しいバージョンではシンプルさを優先して 実装していない.
  4. hyper について 低レベルとはいえちょっとした HTTP リクエストを送受信するくらいなら十分使える. 以下はHTTPリクエストを送る例(一部割愛). let client: hyper::Client<HttpsConnector<HttpConnector>, Body>

    = todo!(); let payload = serde_json::to_vec(&payloadable).unwrap(); let req = Request::builder() .uri(endpoint) .method("POST") .header(CONTENT_TYPE, "application/json") .header(ACCEPT, "application/json") .body(Body::from(payload)) .unwrap(); let res = client .request(req) .await .unwrap(); ref: https://github.com/i10416/firebase-messaging- rs/blob/57279c2bb2aed2782ab679eff98bf7ea813cffef/src/lib.rs#L83
  5. hyper について 非同期処理の場合も雑に clone できるので安心. Client is cheap to clone

    and cloning is the recommended way to share a Client . 内部の Pool は Arc<Mutex<_>> で囲まれている. pub(super) struct Pool<T> { // If the pool is disabled, this is None. inner: Option<Arc<Mutex<PoolInner<T>>>>, } ref: https://github.com/hyperium/hyper/blob/a22c5122e1d2d58e3f30d059978c3eed14cca0 82/src/client/pool.rs#L19
  6. Rust x gRPC prost: Rust の Protocol Buffers の実装と codegen

    tonic: tower と hyper をベースにした gRPC 実装と codegen prost は protobuf の読み取りと、スキーマ → Rust の型へのマッピングを扱う. rpc 部分は個 別の実装に移譲している. tonic は protobuf の rpc 部分の実装を提供する.
  7. Rust x gRPC tonic は tower ベースなので Layer とサービスを組み立ててサーバーを作れる. gRPC

    と gRPC-Web に対応するのも比較的簡単. #[cfg(not(feature = "grpc-web"))] let server = Server::builder() .layer(TraceLayer::new_for_grpc()); #[cfg(feature = "grpc-web")] let server = Server::builder() .accept_http1(true) .layer(TraceLayer::new_for_http()) .layer(GrpcWebLayer::new()); server .layer(JWTVerificationLayer::new(..)) .add_service(health_service) // ... .serve()
  8. Useful crates for Rust on GCP tonic-health grpc health protocol

    の実装. GKE で healthcheck するときに使える. gcloud-sdk-rs googleapis から自動生成された GCP の認証処理、API クライアントや型. Workload Identity Federation(OIDC) での認証も実装されているので嬉しい. firestore-rs gcloud-sdk-rs のラッパー. serde サポートや fluent な API. tracing+tracing-stackdriver 構造化ロギング. 雑にアノテーションをつければいい感じにリクエストのログを取っ てくれる. tracing-stackdriver はGCPが期待する形式にログを整形してくれる.
  9. Gotchas gcloud-sdk-rs は tonic を re-export していないので、tonic のバージョンを gcloud-sdk-rs に揃える必要がある.

    参考: 公開APIのインターフェースで利用している外部クレートはRe-exportする(と良さそ う)
  10. Gotchas generate_id_token が tonic::Request を公開されたインターフェースで受け取るの で、利用している tonic のバージョンの影響を受ける. gcloud-sdk-rs が

    [email protected] に、自 分のアプリケーションが [email protected] に依存しているとコンパイルエラー. let client: GoogleApi<IamCredentialsClient<GoogleAuthMiddleware>> = GoogleApiClient::from_function( IamCredentialsClient::new, "https://iamcredentials.googleapis.com/v1", None, ) .await .unwrap(); let req: GenerateIdTokenRequest = todo!(); match client .get() .generate_id_token(tonic::Request::new(req)) .await
  11. Gotchas tower middleware は async な関数を適用する API(あるいは Layer) がないので JWT

    の検証 などI/O副作用を伴う可能性がある処理は自前で実装しないといけない. 一応 async な Layer をサポートする野良 crate もあるがちょっと不安...
  12. tower-asyncを使わない場合↓こんな感じで結構ややこしい trait を実装しないといけない. impl<S> Service<hyper::Request<Body>> for JWTVerification<S> where S: Service<hyper::Request<Body>,

    Response = hyper::Response<BoxBody>> + Clone + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = futures::future::BoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, mut req: hyper::Request<Body>) -> Self::Future { let clone = self.inner.clone(); let mut inner = std::mem::replace(&mut self.inner, clone); // ... // ... todo!();
  13. ビルドが遅い 不要な crate を取り除こう nix-shell -p cargo-udeps -p iconv cargo

    +nightly udeps CI ではSwatinem/rust-cache を使おう このレポジトリ にあるような cache 生成用のアクションを作り、CI ではそれを利用す るといい. ref: https://github.com/Swatinem/rust-cache/issues/95 ※ GitHub Actions はブランチ間のキャッシュの共有に制限がある. base branch のキャッシュ をうまく利用できるようにしないとキャッシュミスする. https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed- up-workflows#restrictions-for-accessing-a-cache
  14. コンパイル時の保証や抽象化が欲しいなら Scala でいいのでは? Rust の web エコシステムが提供するものとほぼ同じメンタルモデルのエコシステムがある. GC があり lifetime

    を考えなくていい. JVM なので速い. ツールチェーンも揃っている. Rust Scala A => F[B] + HTTP tower, hyper http4s gRPC tonic fs2-grpc, http4s-grpc 非同期ランタイム( F[_] ) tokio cats-effect 宣伝: Rustacean のための Scala 3 入門
  15. Learning Resources tower-http・axum・tonic の利用例: https://github.com/tower-rs/tower- http/tree/master/examples hyper の examples: https://github.com/hyperium/hyper/tree/master/examples

    prost の examples: https://docs.rs/prost-build/latest/prost_build/ tonic の examples: https://github.com/hyperium/tonic/tree/master/examples
  16. おまけ: Rust 向け nix flake { description = "Flake to

    manage Rust workspace."; inputs.nixpkgs.url = "nixpkgs/nixpkgs-unstable"; inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.rust-overlay.url = "github:oxalica/rust-overlay"; outputs = { self, nixpkgs, rust-overlay, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs { inherit system overlays; }; in { devShell = pkgs.mkShell { name = "rust-shell"; buildInputs = with pkgs; [ rust-bin.beta.latest.default # or rust-bin.nightly.latest.default cargo-udeps iconv rust-analyzer ]; }; }); }