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

Overview: Developing gRPC Apps on GCP in Rust

110416
September 29, 2023

Overview: Developing gRPC Apps on GCP in Rust

Overview: Developing gRPC Apps on GCP in Rust from my talk@[2023/09/29 人工衛星の開発現場でLT大会 〜Rust バックエンド開発特集〜](https://arkedgespace.connpass.com/event/292290/)

110416

September 29, 2023
Tweet

More Decks by 110416

Other Decks in Technology

Transcript

  1. Rust@Work I'm building a mobile backend for frontend server in

    Rust, which communicates with client apps over gRPC. The server is running on Cloud Run(dev) and internal GKE clusters(staging and production). In addition, I'm developing a CLI for developers, markup parser and AST transformers in Rust.
  2. Rust x Web Browsing several GitHub repositories and Reddit, the

    following libraries are the de-facto stack for backend development as of 2023. tower-service tower tower-http hyper axum tonic reqwest warp etc.
  3. Rust x Web Most of web ecosystem is built on

    top of tower and tower_service. tower_service tower tower-http hyper axum warp tonic reqwest etc.
  4. tower is the most abstract and fundamental layer. tower is

    the most abstract and fundamental layer. It has a signiture: async fn(Request) -> Result<Response, Error> . It accepts inputs of type Request and returns either successful value of type Response or failed value of type Error in async context. Request ・ Response ・ Error here are protocol agnostic generic types. In other words, they are not tied to HTTP. tower has a concept of layer: Request -> Request , Response -> Response , which allows developers to share common concerns between tower services.
  5. tower: tower-http tower-http is a crate that provides a set

    of HTTP utilities such as logging, compression and header manipulation. Any service that implements tower service can use them as a layer.
  6. hyper hyper is a (relatively low-level)crate that takes care of

    HTTP concerns. hyper is a relatively low-level library, meant to be a building block for libraries and applications. Axum and Warp, well-known Rust web frameworks, and reqwest HTTP client are based on hyper. The latest hyper uses its own service interface instead of tower_service for simplicity through older hyper depends on tower_sercice.
  7. hyper http client example Though it claims itself low-level, it

    is not hard to write a snippet for HTTP request using hyper. 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
  8. hyper is easy to share between async tasks Client is

    cheap to clone and cloning is the recommended way to share a Client . In fact, the internal pool is wrapped inside 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
  9. Rust x gRPC prost: Rust's Protocol Buffers implementation and codegen

    tonic: Rust's gRPC implementation and codegen based on tower and hyper Prost reads and parses protobuf schema into Rust types. It delegates the rpc parts to respective implementations. For example, tonic provides gRPC implementation.
  10. Rust x gRPC tonic is based on tower too. Developers

    can use tower layers to add features to a server in a composable way. The following example demonstrates how easy it is to support both gRPC and gRPC web with a few lines. #[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()
  11. Useful crates for Rust on GCP There are some crates

    useful for developing gRPC web apps on GCP. tonic-health gcloud-sdk-rs types and RPCs generated from googleapis proto and REST api schema with Workload Identity Federation(OIDC) support firestore-rs ergonomic gcloud-sdk-rs wapper with fluent syntax and serde. tracing+tracing-stackdriver Easy structured logging compatible with stackdriver logging format.
  12. Gotchas gcloud-sdk-rs does not re-export tonic, which causes compile error

    due to tonic version mismatch when there are different versions of tonic. 参考: 公開APIのインターフェースで利用している外部クレートはRe-exportする(と良さそ う)
  13. Gotchas In the following example, generate_id_token leaks tonic::Request type in

    public interface. When your application depends on tonic of a version different from one gcloud- sdk-rs depends on, it won't compile. 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
  14. Gotchas It is a bit hard to implement a tonic

    layer which entails I/O operation such as JWT verification because tower middleware does not provide easy API to implement async layer. there is an async layer helper crate...but it does not seem officially supported.
  15. Without tower-async, you must implement this complex trait by yourself.

    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!();
  16. Rust x Web: the good parts just be careful in

    service boundaries(e.g. rpc, file and db i/o) and you get strong type safety in the rest of codebase easy to build, easy to deploy with container async runtime has define-and-run semantics powerful yet concise syntax with algebraic data types and pattern matching
  17. Rust x Web: the bad(?) parts slow build hard( or

    intimidating) to some devs more "managed" functional languages would fit better for some usecases
  18. Slow Build remove unused crates nix-shell -p cargo-udeps -p iconv

    cargo +nightly udeps use Swatinem/rust-cache on CI setup cache generate action as in this repository ref: https://github.com/Swatinem/rust-cache/issues/95 ※ GitHub Actions restrict users from sharing cache between branches. Make good use of cache from base branches on PRs to avoid cache miss. https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed- up-workflows#restrictions-for-accessing-a-cache
  19. hard( or intimidating) In general, writing an application is different

    from writing a library. Developers use only a subset of language features, which is not so hard even for newcomers, for appliation development.
  20. more "managed" languages would fit better for some usecases Other

    functional languages, such as Scala, also give similar compile time safety and higher abstraction with less cognitive cost and less steep learning curve(at the cost of performance overhead). In particular, Scala has a great ecosystem that corresponds to tokio and tower ecosystem. Rust Scala A => F[B] + HTTP tower, hyper http4s gRPC tonic fs2-grpc, http4s-grpc Async Runtime( F[_] ) tokio cats-effect Ad: Rustacean のための Scala 3 入門
  21. bonus: nix flake for Rust { 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 ]; }; }); }