Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

estie、Rustで新プロダクト作るってよ - 2022/2/16

estie、Rustで新プロダクト作るってよ - 2022/2/16

2022/2/16に開催されたイベントで使用した資料です。
https://seriesa.estie.jp/event_rust

# イベント概要
estie(エスティ)は2022年1月12日、約10億円のシリーズA資金調達を発表いたしました。調達した資金を活用して、商業用不動産業界のデジタルトランスフォーメーションをより加速すべく、主力サービスである「estie pro」を拡張するマルチプロダクト戦略のもと、新しいプロダクトをどんどん開発しています。
そのうちの1つは、最近注目があつまるRustをバックエンドに採用したWebアプリです。どのようにRustを活用してWebアプリを開発するのか、新しい言語としてRustを採用したestieでの例をご紹介します!

# 企業ページ
https://www.estie.jp/corp/

estie | エスティ

February 16, 2022
Tweet

More Decks by estie | エスティ

Other Decks in Programming

Transcript

  1.  ຊ೔ͷྲྀΕ  Φʔϓχϯά r FTUJFͱ͸ʁ <NJO>  LFOLPPPP ʰ3VTUͰ΢ΣϒΞϓϦʱ<NJO

    NJO2">  ฏా౦ເʰ3VTUº/FYUKTº"VUIͰϢʔβʔೝূʱ<NJO NJO2">  શମΛ௨ͯ͠ͷ࣭ٙɾΫϩʔδϯά <NJO> )BTIUBHFTUJF@ST 2"ػೳΛͥͻ͝׆༻͍ͩ͘͞ʂ
  2. 

  3. 20/4 20/5 20/6 20/7 20/8 20/9 20/10 20/11 20/12 21/1

    21/2 21/3 21/4 21/5 21/6 21/7 21/8 21/9 21/10 21/11  ϏδωεΠϯύΫτ ओͳಋೖاۀ ԻॱɺҰ෦ൈਮ BOENPSF .33 Y ௚ۙϲ݄
  4.  3VTUͷಛ௕  $ ฒʹ଎͍  ΋ͱ΋ͱ'JSFGPY༻ݴޠ  -JOVYΧʔωϧʹ࢖ΘΕͦ͏ 

    "84Ͱ͸7.΍04ͳͲʹ࢖ΘΕ͍ͯΔ  ίϯύΠϥ͕Ϧιʔε؅ཧʹ͍ͭͯνΣοΫ͠·͘Δ
  5.  void listOperation(List<String> list) { list.add("Awesome!"); } Javaではよく⾒る関数だが… • 実は

    List がミュータブルであることを強制 • ImmutableList を渡すと Runtime Exception • 呼び出し元でも予期しないかも?
  6.  void listOperation(List<String> list) { list.add("Awesome!"); } Javaではよく⾒る関数だが… • 実は

    List がミュータブルであることを強制 • ImmutableList を渡すと Runtime Exception • 呼び出し元でも予期しないかも? →コメントをつける・関数名を⼯夫するしかない
  7.  fn list_operation(list: Vec<String>) {…} • 呼び出し元では list は使われない •

    move しているので fn list_operation(list: &Vec<String>) {…} • イミュータブルな参照 fn list_operation(list: &mut Vec<String>) {…} • ミュータブルな参照
  8.  3VTUͰ΢ΣϒΞϓϦ • ΢ΣϒϑϨʔϜϫʔΫBDUJYXFC • +BWBͷ 4QSJOHɺ1ZUIPOͷ 'MBTL • 42-υϥΠόʔTRMY

    • .Z42-Ͱ࢖༻ • 03.Ͱ͸ͳ͘ɺΫΤϦ௚ॻ͖ • 4ΫϥΠΞϯτSVTPUP • ݹࢀ "84ϥΠϒϥϦ
  9.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) }
  10.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) } JSONとして受け取るように指定 FWでパース等やってもらう
  11.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) } FWからSQLコネクションプールを もらってくる
  12.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) } ヘッダからトークンを取り出して IDを取得しておいてもらう
  13.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) } 返り値は Result
  14.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) }
  15. 

  16.  3VTUʹ͓͚Δ    3FTVMUܕͷ FBSMZSFUVSO value, err :=

    f() if err != nil { return nil, err } let value = f()?;
  17.  #[post("/api/buildings")] pub async fn create_building( request: Json<CreateBuildingRequest>, pool: Data<MySqlPool>,

    auth0_id: ReqData<Auth0Id>, ) -> Result<HttpResponse> { let building = Building::from(request.building); verify_user(&pool, &auth0_id.0).await?; let id = Building::insert(&pool, &building).await?; Ok(Json(CreateBuildingResponse { building_id: id })) } マクロで⾊々⽣成してもらう
  18.   async fn insert(pool: &MySqlPool, unit: &Unit) -> Result<u64>

    { let unit_id = sqlx::query(r#" INSERT INTO units ( area, floor, building_id ) VALUES (?,?,?)"#, ) .bind(unit.area) .bind(unit.floor) .bind(unit.building_id) .execute(pool) .await? .last_insert_id(); Ok(unit_id) }
  19.   async fn find_by_bid(pool: &MySqlPool, bid: i64) -> Result<Vec<RawUnit>>

    { let units = sqlx::query_as!( RawUnit, "SELECT * FROM units WHERE building_id=?", bid ) .fetch_all(pool) .await?; Ok(units) }
  20.   async fn find_by_bid(pool: &MySqlPool, bid: i64) -> Result<Vec<RawUnit>>

    { let units = sqlx::query_as!( RawUnit, "SELECT * FROM units WHERE building_id=?", bid ) .fetch_all(pool) .await?; Ok(units) }
  21.   sqlx::query_as!( RawUnit, "SELECT * FROM units WHERE building_id=?",

    bid ) struct RawUnit { unit_id: i64, area: Decimal, floor: u8, building_id: i64, }
  22.   sqlx::query_as!( RawUnit, "SELECT * FROM units WHERE building_id=?",

    bid ) struct RawUnit { unit_id: i64, area: Decimal, floor: u8, building_id: i64, } もし NULL が含まれていたら……?
  23.   sqlx::query_as!( RawUnit, "SELECT * FROM units WHERE building_id=?",

    bid ) struct RawUnit { unit_id: i64, area: Decimal, floor: u8, building_id: i64, } ↑ このクエリの返り値の型が RawUnit と同じか、 コンパイル時にチェックする。
  24. ೝূϑϩʔ Backend ID Provider BFF Frontend ID Token ID &

    Password セッションCookie JWK ID Token トークンを検証 レスポンス
  25. ϑϩϯτΤϯυ͔ΒͷϩάΠϯ Backend ID Provider BFF Frontend ID Token ID &

    Password セッションCookie トークンはサーバーサイド で暗号化・復号化
  26.  *%τʔΫϯΠϯ ペイロード部分の例 +855PLFOͷߏ଄ • ϔομʔ෦ • ॺ໊ݕূ৘ใΛؚΉ • ϖΠϩʔυ෦

    • ΞΧ΢ϯτͷଐੑ৘ใͳͲΛؚΉ • ॺ໊෦ • ೝূࡁΈͰ͋Δ͜ͱΛݕূ͢Δॺ໊ΛؚΉ
  27.  /FYUKTº"VUIΠϯ "VUI3FBDUKT 4%, IUUQTHJUIVCDPNBVUIBVUISFBDU • /FYUKTͰ4UBUJD)5.-&YQPSUΛ࢖༻͍ͯ͠Δ৔߹ • αʔόʔαΠυϨϯμϦϯά࣌ʹϢʔβʔσʔλʹΞΫηε͢Δඞཁ͕ͳ͍৔߹ •

    /FYUKTͷ"1*3PVUFTΛϓϩΩγͱͯ͠࢖༻ͯ͠֎෦"1*Λݺͼग़͢ͷͰ͸ͳ͘ɺΞΫηε τʔΫϯΛऔಘͯ͠ϑϩϯτΤϯυϨΠϠʔ͔Β௚઀֎෦"1*Λݺͼग़͍ͨ͠৔߹ "VUI/FYUKT 4%, IUUQTHJUIVCDPNBVUIOFYUKTBVUI • ͦΕҎ֎ͷ৔߹ • $PPLJFͰͷηογϣϯͷ؅ཧ͕Մೳ
  28. όοΫΤϯυͰͷϢʔβʔͷೝূ pub struct Auth0Middleware<S> { service: Rc<S>, client: Rc<Auth0Client>, }

    impl Auth0Middleware<S> { fn call(&self, req: ServiceRequest) -> Self::Future { let token = parse_bearer_token(&req)?; let auth0_id = match client.get_auth0_id_from_bearer(token); req.extensions_mut().insert(auth0_id); service.call(req).await.map(|res| res.map_into_left_body()) } }
  29. όοΫΤϯυͰͷϢʔβʔͷೝূ pub struct Auth0Middleware<S> { service: Rc<S>, client: Rc<Auth0Client>, }

    impl Auth0Middleware<S> { fn call(&self, req: ServiceRequest) -> Self::Future { let token = parse_bearer_token(&req)?; let auth0_id = match client.get_auth0_id_from_bearer(token); req.extensions_mut().insert(auth0_id); service.call(req).await.map(|res| res.map_into_left_body()) } } HTTPの Authorization HeaderからJWT トークンを抽出
  30. όοΫΤϯυͰͷϢʔβʔͷೝূ pub struct Auth0Middleware<S> { service: Rc<S>, client: Rc<Auth0Client>, }

    impl Auth0Middleware<S> { fn call(&self, req: ServiceRequest) -> Self::Future { let token = parse_bearer_token(&req)?; let auth0_id = match client.get_auth0_id_from_bearer(token); req.extensions_mut().insert(auth0_id); service.call(req).await.map(|res| res.map_into_left_body()) } } JWTトークンを検証 し、auth0のIDを抽 出する
  31. όοΫΤϯυͰͷϢʔβʔͷೝূ fn parse_bearer_token(request: &ServiceRequest) -> Result<&str> { let header =

    request.headers() .get(&actix_web::http::header::AUTHORIZATION)?; let mut parts = header.to_str()?.splitn(2, ' '); let token = parts[1].ok_or_else(|| anyhow::anyhow!("no token"))?; Ok(token) } • BDUJYXFCͷBQJΛ༻͍ͯ"VUIPSJ[BUJPOϔομʔΛநग़ • ϔομʔͷܗࣜ͸ #FBSFSYYYYYYYYYYYYY
  32. όοΫΤϯυͰͷϢʔβʔͷೝূ +85τʔΫϯΛݕূ͢ΔΫϥΠΞϯτ pub struct Auth0Client { client: reqwest::Client, base_url: String,

    } impl Auth0Client { pub(super) async fn get_auth0_id_from_bearer(&self, bearer_token: &str) -> Result<String> { self.validate_jwt_token(bearer_token).await?.sub //JWT payloadのsub } /////ここに⾊々なメソッド }
  33. όοΫΤϯυͰͷϢʔβʔͷೝূ +85τʔΫϯΛݕূ͢ΔҰ࿈ͷॲཧ async fn validate_jwt_token(&self, token: &str) -> Result<JWTPayload> {

    let jwks = self.fetch_jwks().await?; let kid = match decode_header(token)?.kid.unwrap(); let jwk = match jwks.find(&kid) { Some(res) => res, None => return Err(anyhow::anyhow!("Specified key not found in set")), }; Auth0Client::dec_jwt(jwk, token) }
  34. όοΫΤϯυͰͷϢʔβʔͷೝূ +85τʔΫϯΛݕূ͢ΔҰ࿈ͷॲཧ async fn validate_jwt_token(&self, token: &str) -> Result<JWTPayload> {

    let jwks = self.fetch_jwks().await?; let kid = match decode_header(token)?.kid.unwrap(); let jwk = match jwks.find(&kid) { Some(res) => res, None => return Err(anyhow::anyhow!("Specified key not found in set")), }; Auth0Client::dec_jwt(jwk, token) } Auth0に検証⽤の鍵 をリクエスト
  35. +85τʔΫϯΛݕূ͢ΔҰ࿈ͷॲཧ async fn validate_jwt_token(&self, token: &str) -> Result<JWTPayload> { let

    jwks = self.fetch_jwks().await?; let kid = match decode_header(token)?.kid.unwrap(); let jwk = match jwks.find(&kid) { Some(res) => res, None => return Err(anyhow::anyhow!("Specified key not found in set")), }; Auth0Client::dec_jwt(jwk, token) } 署名部を⽤いて トークンを検証 όοΫΤϯυͰͷϢʔβʔͷೝূ
  36. +8,ΛϦΫΤετ͢Δ async fn fetch_jwks(&self) -> Result<Jwks> { let uri =

    format!("{}/.well-known/jwks.json", self.base_url); let res = self.client.get(uri).send().await?; let val = res.json::<Jwks>().await?; Ok(val) } όοΫΤϯυͰͷϢʔβʔͷೝূ
  37. όοΫΤϯυͰͷϢʔβʔͷೝূ +85τʔΫϯΛݕূ͢Δ fn dec_jwt(jwk: &Jwk, jwt: &str) -> Result<JWTPayload> {

    match decode::<JWTPayload>( jwt, &DecodingKey::from_rsa_components(&jwk.n, &jwk.e), &Validation::new(Algorithm::RS256), ) { Ok(c) => Ok(c.claims), e => Err(anyhow::anyhow!("failed to decode jwt: {:?}", e)), } }
  38. όοΫΤϯυͰͷϢʔβʔͷೝূ BQJଆͰBVUI@JEΛ࢖༻͢Δ #[post("/api/buildings")] pub(crate) async fn create_building( request: web::Json<CreateBuildingRequest>, pool:

    web::Data<MySqlPool>, auth0_id: web::ReqData<Auth0Id>, ) -> ApiResult<impl Responder> { let request = request.into_inner(); let building_info = BuildingInfo::try_from(request.building_info).invalid_request()?; verify_user(pool.as_ref(), &auth0_id.0).await?; let building_id = Building::insert(pool.as_ref(), &building_info) .await .log_db_err()?; Ok(web::Json(CreateBuildingResponse { building_id })) }