【23卒】エンジニアの就活した後に考える就活の軸

こんにちは!技術開発部の北條です。

今回は僕が改めて新卒としてエンジニア就活をするならどういったことを重要視するかを考えてみました。 インターンとして5ヶ月経過し、就活の時点で「知っておければ良かった、勉強しておければ良かった」という点があるので紹介します。

尚、この記事はメディアチームでの経験なので他のチームとは異なる部分があるかもしれませんが予めご了承ください。

掲げていた就活の軸

僕は「人生を豊かにするようなサービスを提供する」「ビジネスサイドとの距離が近い開発体制がある」の2点を就活の軸として掲げていました。 vivit.hatenablog.com

大学の授業でC言語を触っていたものの、本格的に勉強を始めたのが大学3年生からなのでWeb系を目指すには時期的に遅い分類になります。

技術力で、企業に貢献することはおろかアピールすることも難しいだろうと思っていました。

そのため、新卒のころからサービス志向のマインドを培い、将来は技術力の高いエンジニアの方々をまとめるPM、PLのような立場で企業に貢献しようと考えていました。

改めて就活するなら

「人生を豊かにするようなサービスを提供する」は変わらないと思います。

「ビジネスサイドとの距離が近い開発体制がある」は軸としては間違っていないのですが、それ以前にエンジニア内での開発体制を確認すべきだと思いました。

加えて下記2点について考慮すべきだったり、考えが変わったので紹介します。

技術について

そもそも技術に対しての知識や経験が乏しいため言語の他に何を見たらよいのかわからなかったのです。 今であれば、開発体制や技術的負債について考えると思います。

もちろんモダンな言語を選ぶに越したことはないのですが、言語のバージョンを古いままにしてないか、リファクタリングを積極的に行う文化があるかなどの技術的負債についての意識が高いかを確認します。

バージョンが古いが故に非推奨のものを使い続けたり、負債の影響でさらに負債を増やすような実装を強いられたりしてはエンジニアとしての成長速度は遅くなってしまいます。

vivitはサービス志向であるもののバージョンアップをしっかりと行ったり、技術的負債を残さない(あるいは後に解消する)という姿勢があります。 サービス志向だけを優先するのではなく、技術に関する姿勢もカジュアル面談などで確認しておきたいポイントだなと思いました。

キャリアプランについて

以前はリーダー的な立ち位置で補佐的にコードを書くというキャリアプランを掲げていました。 しかし、5ヶ月働いてみてやっぱりコードを書くこと自体が好きだなと強く感じています。

今後も変わるかもしれませんが、今はビジネス視点も持ったスペシャリストになりたいと思うようになりました。

キャリアプランとは正確に行き着くための地図ではなく、方向を示す羅針盤なのだと考えれば精神的にも余裕のある就活を送れたのではと思いました…笑

キャリアプランに正解はありませんし、大人達も答えを知っている訳ではありません。 そのため就活中はキャリアプランの内容ではなく、なぜそれを掲げているのか抽象化できていることが重要だと感じました。

最後に

技術とキャリアプランについてはvivitのインターンだからこそ感じたものです。

僕は50社程度しか会社を見ていませんし比較できるほどの経験はないのですが、大変満足のいく就活ができたと思っています。

vivitも会社として完璧ではありません。しかし、インターン生でありながら組織の改善に携わることができるのはベンチャーならではだと思うので、より早く貢献できるように引き続き頑張ります!

【Next.js】Firebase Authentication でパスワードの変更を実装する

はじめに

フロントエンドエンジニアの関(@kur0buchi)です。

hinataレンタルでは先日会員機能をリリースいたしました。 当記事は会員機能実装にあたり調査していく上で適当なパスワード変更のサンプルを探しても現在の環境に丁度よく流用できそうなものが見当たらなかったため執筆に至りました。

hinata-rental.me

前提

  • Next.js v9 以降
  • TypeScript v4.2.3
  • Firebase v9

この記事で利用している各技術の詳細な説明は省きます。

利用例

パスワード変更機能にも様々あると思いますが、今回は「現在のパスワード」と「新しく設定するパスワード」を渡す形式を採用することとします。

使い方としては旧パスワードと新パスワードを渡すと Promise<void> が返ってきて何かあったら catch できるくらい簡単な使用感を目指します。

バリデーションやエラーチェックは適宜行ってください。

import { updatePassword } from "path/to/logic";

export const Form = () => {
  const handleSubmit = async (oldPassword: string, newPassword: string) => {
    // パスワード更新処理
    await updatePassword(oldPassword, newPassword)
      .catch((e) => {
        // エラー処理
      })
      .then(() => {
        // 成功時処理
      });
  };

  return (
    <div>
      <button
        onClick={() => {
          handleSubmit("hoge", "fuga");
        }}>パスワード変更</button>
    </div>
  );
};

実装

下記の記事を参考にして、同じ使用感でパスワード変更のメソッドを追加する方針にしています。

zenn.dev

ディレクトリ名は一例です

// auth/app.ts
import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const firebaseApp = initializeApp(firebaseConfig);

export default firebaseApp;
// auth/index.ts
import {
  EmailAuthProvider,
  getAuth,
  reauthenticateWithCredential,
  updatePassword as firebaseUpdatePassword,
} from "firebase/auth";
import firebaseApp from "./app";

export const updatePassword = (
  oldPassword: string,
  newPassword: string
): Promise<void> => {
  return new Promise((resolve, reject) => {
    const auth = getAuth(firebaseApp);
    if (auth.currentUser == null) {
      return reject();
    }
    
    // クレデンシャルの取得
    const credential = EmailAuthProvider.credential(
      auth.currentUser.email || "",
      oldPassword
    );
    
    // メールアドレスの再認証
    reauthenticateWithCredential(auth.currentUser, credential)
      .then((userCredential) => {
      
        // パスワードの更新
        firebaseUpdatePassword(userCredential.user, newPassword)
          .then(() => resolve())
          .catch((error) => reject(error));
      })
      .catch((error) => reject(error));
  });
};

特筆するようなことはありませんが、クレデンシャルの取得、メールアドレスの再認証、パスワードの更新を順に処理しています。

firebase/auth/updatePassword をインポートする際のエイリアス命名の都合なので必須ではありません、お好みでどうぞ。

終わりに

vivit ではモダンな開発環境で活躍したいエンジニアを大募集中です。

www.wantedly.com

参考

最高の Kubernetes ダッシュボードを求めて ~2022春~

こんにちは。 vivit で SRE をやっている 宮本 です!

素晴らしい Kubernetes(以降 k8s) ライフを送るために欠かせないのがダッシュボードです!

ダッシュボードには見やすさ、網羅性、操作性など沢山のものが求められます。

k8s の場合はさらに、多くのコンポーネント(Deployment、それに紐づく ReplicaSet 、 Pod など)が絡み合っており、使いやすいダッシュボードを作るのは結構難しいです。

一般的なダッシュボードと違って、k8s ではリソース(特に Pod)の更新やオートスケーリングが頻繁に行われるため、今現在の Cluster の状態を自動更新で即座に表示してくれるものだと嬉しいです 🙆‍♂️

「普通に kubectl で良くない?」と思っていましたが、やはりダッシュボード使わないと冗長に感じてしまいます。


そんなこんなで、以下のざっくりとした観点から、(個人的)最高の k8s ダッシュボードを検討してみたいと思います!!

項目 説明
見やすさ 色、アイコン、レイアウトなどで見やすいものになっているか
操作性 操作は直感的か
ショートカットキーが十分使えるか
反映の速度・安定性 リアルタイムに自動更新されるか
反映速度が十分で、変に遅延したり不安定だったりしないか
セットアップの手間 どれだけセットアップの労力がかからないか

なお、評価は以下のように行います。

絵文字 評価
🤩 完璧!言うこと無し!
🙆‍♂️ 完璧じゃないけど、良いね
🙅‍♂️ 改善の余地が大きい

検討対象

GKE ダッシュボード

項目 評価 コメント
見やすさ 🤩 GKE に限らずですが、GCP のコンソール画面は本当に見やすいです!
完結にまとめられていて、美しいです。
操作性 🙆‍♂️ 操作は直感的で迷うことはありません。
GCP だからこそ、LB や証明書の画面にすぐ移動できるのも素晴らしいです。
ブラウザなので仕方ないですが、ショートカットキーがほとんど使えないのが難点です。
反映の速度・安定性 🙅‍♂️ 安定していますが、リアルタイム更新機能はありません。
また、UI のためなのかページの読み込み速度がちょっと遅く感じることが多いです。
セットアップの手間 🤩 GKE を使うなら、正真正銘の手間0です!

Lens

項目 評価 コメント
見やすさ 🤩 情報が完結にまとめらています
落ち着いたデザインで個人的にもすごく好みです!
操作性 🙅‍♂️ 画面操作は分かりやすいのですが、自分が触った限りだとショートカットキーがあまり充実してなさそうでした(知らないだけだったらすみません)
デスクトップアプリなので、ここは残念なポイントでした。
反映の速度・安定性 🤩 リアルタイム更新が出来るのが素晴らしいです!
速度も問題無く、言うこと無しです。
セットアップの手間 🙆‍♂️ デスクトップアプリなので、別途インストールが必要です。
ただし homebrew で入れられますし、kubectl 以外の設定も特に不要なので簡単です。

k8slens.dev

IDE と記載がありますが、これで何か開発するわけではありません。
普通のダッシュボードだと思って頂ければOKです。

補足ですが、Prometheus を使って Pod メトリクスを見る機能も付いています!
vivit では Datadog を使っていて Prometheus は未使用なので恩恵は特に無いです。

IntelliJ IDEA k8s プラグイン

私は IntelliJ を使っていますが、VSCode でも同じようなことが出来ると思います!

項目 評価 コメント
見やすさ 🙆‍♂️ 割と最低限で、特に見やすいということはありません。
必要な情報はしっかりまとまっているので問題はありません。
操作性 🤩 普段と同じ IDE の操作感でショートカットキーが使えます!
Pod の検索などもお手の物で、操作性は素晴らしいです。
反映の速度・安定性 🤩 リアルタイム更新はできません。
ただし Refresh をショートカットキーで行えるので問題無く、反映速度も早いです。
セットアップの手間 🤩 プラグインを有効にするだけなのでほぼ0です。

Pod の中に入る、ライブマニフェストを出力してくれるなど便利な機能もサクサク使えます。

Datadog

項目 評価 コメント
見やすさ 🤩 流石は Datadog で、情報のまとめ方、 UI が抜群です。
操作性 🙆‍♂️ 一部しかショートカットキーが使えません。
が、メトリクスやログなどへのジャンプが強力です。
反映の速度・安定性 🙅‍♂️ リアルタイム更新に対応しています!
しかし反映までの速度が遅いのと、削除済の Pod が表示されたり、Complete ステータスになっている Job が runnning のまま表示されるなど安定感がいまいちです。
セットアップの手間 🙅‍♂️ ダッシュボードの枠を超えているので当然ですが、k8s cluster にエージェントを入れる必要があります。
料金との相談も必要で、手間はそれなりにかかります。

「Datadog は k8s ダッシュボードツールじゃないだろ!」と突っ込まれるかもしれませんが、ダッシュボードとしても使えます。
以下のように k8s タブが存在しており、マニフェストファイルを見たり Pod の状態を確認したりと一通りのことはできます!

Datadog の強みは何と言ってもメトリクス、ログとの連携です! ここは他では再現できないレベルで素晴らしいですが、やはり料金面で選択肢に入らない場合もあると思います。

優勝決定 & 総括

優勝は、、、

IntelliJ IDEA k8s プラグイン です🥇🥇🥇

やはりショートカットキーの充実とブラウザへ切り替えなくて良い手軽さが優勝のポイントです!!
メトリクスはどうしても見られないので、必要であれば Pod 名などをコピーして Datadog にペーストするという感じです。


vivit では一緒に働くエンジニアを大募集しています 🎉

www.wantedly.com

少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております!

Amazon SNSでiOS Push通知を証明書ではなく認証キーで認証する

はじめに

こんにちは、技術開発部 事業横断チーム データエンジニアの多田です。
入社時はアプリエンジニアだった私、hinataアプリの保守・運用をたまにやっております。
最近、Amazon SNSiOS Push通知認証方法を証明書形式から認証キー形式に変更しました。
ただ、Amazon SNSでの認証キーを使ったPush通知認証についてネットにあまり情報がなかったため、記事にまとめてみます。

概要

hinataアプリのPush通知にはAmazon SNSを使用しています。

最近、Amazon SNSに登録しているiOS Push通知証明書を更新する必要がありました。
作業する際に、2021年11月にAmazon SNSでも認証キーに使用ができるようになった事に気づきました。 これを機に証明書による認証から更新が不要な認証キーによる認証に変更する事としました。(参考

本記事ではその変更作業の内容及びトラブルシューティングを記載します。

Push通知の証明書?認証キー?

iOSアプリでPush通知を利用する場合、Apple Push Notification Service(以下、APNs)にリクエストを送ります。
この時、Appleに対して認証が必要で方法は下記2つあります。

  • 発行から1年の有効期限があるSSL証明書
  • 有効期限がない認証キー

証明書の方が古いやり方で、現在は認証キーの方をAppleは推奨しているそうです。
ちなみに、証明書の方は環境毎(product, staging, sandbox等々)に発行する必要がありました。
ですが、認証キーはベンダー共有になるため1個発行すれば使い回せます。

詳細は下記を。とてもわかり易かったです。
【iOS】Firebase Cloud Messagingで利用するAPNs認証キー・証明書の作り方 - Qiita

変更作業

Amazon SNSに認証キーを登録する

下記、設定を間違えると全デバイスにPush通知が届かなくなる恐れがあるため、慎重に。

手順

  1. APNs認証キーをAppleDeveloperから発行する(手順に関しては上記Qiitaのリンク参照)
  2. Amazon SNSの管理画面に入る
  3. 左サイドバーMobileプッシュ通知
  4. プラットフォームアプリケーションにある該当アプリケーションを選択
  5. 編集を押して、下記の通り設定
    1. 認証方法: トークン
    2. 署名キー: 1.でダウンロードした.p8ファイルをアップロード
    3. 署名キーID: Certificates, Identifiers & Profiles - Keysで、1で作成したキーを開いて出てくる Key ID
    4. チーム ID: Apple デベロッパーアカウントのMembershipページにある
    5. バンドルID: Certificates, Identifiers & Profiles - Identifierにある該当のアプリを開いて出てくるBundle ID
  6. 入力し終えたら 変更の保存
  7. Push通知を送ってみて届いたら完了。なお、全エンドポイントへのPush通知をしてしまうと、仮に設定が間違っていた場合全てのエンドポイントが無効になってしまう。個別Pushで試す事を推奨。

トラブルシューティング

テストPush通知後、全てのエンドポイントが無効になってしまった

最初、上記手順5で間違ったバンドルIDを登録した後、手順7で全エンドポイントにPush通知を送ってしまいました。
すると、Amazon SNSに登録されたエンドポイントが全て無効になってしまいました…
無効になるとどうなるかというと、認証情報を正しく設定し直したとしてもPush通知がそのエンドポイントへ届かなくなります。

この時はsandbox環境で行っていたため、影響は少なく済みました…本番環境でと思うとゾッとしますね。

なぜ無効になってしまったかというと、無効な認証情報に対してPush通知を行おうとしたためです。
下記、Amazon SNS プッシュ通知のエンドポイント編集画面に記載があるように、無効なPush通知リクエストがあるとAmazon SNSがエンドポイントを自動で無効にしてしまうようです。

エンドポイントへの配信を有効にします。エンドポイントが無効であることを通知サービスが Amazon SNS に示すと、Amazon SNS はこれを false に設定します。

無効になってしまったエンドポイントは、一応エンドポイント編集画面で有効・無効は切り替えられます。
かつ、APIも用意されているため↓、一括で無効→有効に切り替えることも出来ます。
SetEndpointAttributes

とあるSREの自宅PC事情

こんにちは!技術開発部マネージャーをしている井島です。 今回はvivit に私が入社当時(2020年1月)に開発部で社内LTをやっていたことがあり、趣味全開の内容があったので、それを紹介しようと思います!

vivitでは2021年8月からマネージャーを行っていますが、それ以前はSREでした。前職では新卒からインフラエンジニアをやっており、この分野は好きです。

なんだかんだ20万円以上かかっている

まずは全体の写真です。

f:id:ijimakenta:20220410183120p:plain f:id:ijimakenta:20220410193610p:plain

ブログ執筆現在もこの構成なのですが、高スペックPCが買えるくらいですね...

ベアボーンという小さな筐体を使ってます。バッファローのHubはジャンボフレーム対応。電源タップは業務用でも使うサンワサプライ製で雷ガード付き。
ちょっとしたこだわりポイントです。

いわゆるハイパーコンバージドインフラ

f:id:ijimakenta:20220410183448p:plain そもそも、「自分のPCにスケールアウトできるストレージが欲しい」というところから始まりました。

非機能要件はこんな感じです。

  • ハードウェアは交換可能で、一般家庭用向けのものを使用する。
  • ストレージシステムのデータ部分に関してシングルスポットをつくらない。
  • 使わない時はシステムを停止する。
  • HDDレベルのシーケンス読み込み、書き込み性能。
  • ストレージシステム障害時、バックアップデータへは復旧作業なしにアクセス可能。
  • Windows, Linux, Mac, Android の各デバイスからストレージへアクセス可能。

SDS (Software Defined Storage) を使用

ceph.io ceph というミドルウェアを使いました。構築当時、SDS分野が流行っていて、OSSだとこれが一番利用実績があったので選びました。

普通だったらRAIDがまっさきにに思い浮かぶと思いますが、RAIDカードが故障した場合、交換可能かどうか不安があり却下しています。
例えばRAIDカードが故障したときに同じ型のものを調達できるのか?違うRAIDカードに交換したとき、正しくディスクが認識されるのか?など、、、

kubernetes について

ストレージシステム技術スタックの写真で cephの上にk8s があったと思います。これはceph サーバのメトリクスを取得するためと、samba サーバ起動するためです。(これだけのためです笑)

これも当時ちょうど流行っていたってことも加えて、これを使えば prometheus, grafana, samba 起動サーバを cephノード増減に左右されないと見込んで採用しました。

k8s クラスタのマスターノードにについては、使わない時クラスタを停止することを考慮し、クラスタ起因の障害を回避するためにシングル構成にしています。

ディスクトップPCスペック

いつも使うPCです。普通ですよね!?
CPUファンはこだわって、静音、ベアボーン筐体に入るものを探しました。

  • Desk Mini 110/B/BB
  • Core i5 7500 BOX
  • CFD D4N2133PS-8G [SODIMM DDR4 PC4-17000 8GB]
  • NH-L9i(Noctua) ※CPUファン
  • 変換名人のPCIB-USB2/2FL
  • Debian testing

サーバ側スペック

サイジングについて話し出すと長くなりすぎるので、ここではふ〜んくらいでお願いします。

  • Desk Mini 110/B/BB
  • Intel CPU Celeron G4900 BOX
  • crucial CT8G4SFS824A [SODIMM DDR4 PC4-19200 8GB]
  • NVMe タイプ 2280 PCIe Gen3 x4
  • SEAGATE ST1000LM049 [1TB 7mm] 7200rpm
  • SEAGATE ST1000LM049 [1TB 7mm] 7200rpm
  • CentOS 7

最後に

細かい設計思想についても話したいのですが、長くなりすぎるのでここでは紹介だけにとどめておこうと思います。 vivit では中途エンジニアも積極募集中です。面談や面接でお会いしたときなど、興味があれば是非質問して下さい!

www.wantedly.com www.wantedly.com

ちなみに、cephをバージョンアップした時、1回失敗してデータ飛んでいます...
もちろんバックアップから復旧しましたよ。

23卒エンジニアのvivit体験記

こんにちは!技術開発部の北條です。 私は23卒エンジニアとして入社が決まっており、2022年3月からインターンとして参加しています。

今回はvivitへ入社を決めた理由や一ヶ月間のインターン内容、vivitの雰囲気について紹介します。

本ブログでは、21卒の氏家さんが入社した理由やインターンの体験談についての記事も公開されています。私自身、就職活動において「年齢の近い新卒の言葉」がとても参考になったので、この記事が24卒以降の方や技術開発部に興味のある方の参考になれば幸いです。

vivitの内定を決めた理由

私はWantedlyでvivitのミートアップに参加したのが初めての出会いでした。

ミートアップで社員の雰囲気がとても良いと感じて、率直にここで働いたら楽しそうだと思いました。当時、質問タイムで質問が出なくなったときに、人事の宍道さんが自分で質問して自分で回答していたのを今でも覚えています笑

そして、楽しそうという理由以外に内定を決めた理由が二つあるので紹介します。

就活の軸に当てはまっていた

私は就活の軸に下記ニ点を設定していました。

  • 人生を豊かにするようなサービスを提供する

  • ビジネスサイドとの距離が近い開発体制がある

vivitのビジョンである「心動かす体験を通じて、世界をより豊かに」は、人生を豊かにしたいという私の軸と同じ方向を向いていると感じました。

また、ビジネスサイドとの距離感は個人の測り方でも変わってくるので一概には言えないのですが、同じフロアで働いていることや、仕様についてエンジニアが意見を言えることは、私にとって良い環境だと思いました。

hinataのプラットフォーム化

もう一つは、hinataでプラットフォームを作るという考えに自分も携わりたいと思ったからです。vivitではメディア以外にストアレンタルspotリユースと他のサービスも運用しています。

「メディアでキャンプに興味を持って、キャンプに行きたいと思ったらspotでキャンプ場を予約して、ギアはレンタルで借りて、手ぶらでもキャンプを楽しめる。キャンプギアが欲しくなったらストアで購入もできる。今は個々のサービスとして独立しているけど、プラットフォーム化ができればこれらをシームレスに行えるようになる。」

私はこのプラットフォーム化にとてもワクワクして、エンジニアとして携わりたい!と強く思いました。

インターンの内容

vivitの開発体制に慣れるということでRuby on Railsを使用しているWebメディアのhinataに配属されました。私はもともとRubyを学んでおり、Rubyの技術力を上げたいと希望をだしていたので、そちらも反映していただきました。

インターンに参加して驚いたのが、初日から環境構築を始め、二日目にはタスクを任されたことです。教材を使った基礎学習や先輩エンジニアからの指導などは行わず、hinataの開発業務で慣れるという方針なので、「なんて良い環境なんだ…」と思いました。

氏家さんも入社当時に同様のことを仰っています。

4月1日の入社式で自分がインターンのときと同じmediaチーム(主にhinataの業務)への配属が決まった次の日にタスクが振られ、コードを書くことになりました。

ああ、ベンチャーって本当に良いなって思いました。

vivit.hatenablog.com

タスクについてですが、簡単なものから着手していき、Railsの技術やチームでのGitHubの使い方、リリースの仕方を学んでいきました。常に先輩エンジニアが横についてくれているので、調べてもわからない点は気軽に質問することができました。先輩が会議などで忙しいときはSlackに投稿することでチームのエンジニアに回答してもらっています。

最初は毎日出社していましたが、一ヶ月経った今では開発体制にも慣れてきて、リモートで業務を行っています。インターン生でありながら7回リリースを経験し、ユーザーが実際に触る部分も担当しました。タスクの内容もビジネスサイドから要望が入り、仕様が決まった時点で着手するといったスピード感のあるものも担当しています。

vivitの雰囲気

実際にインターンに参加することで感じたvivitの雰囲気についてご紹介します。

デスクはプロダクトごとで分かれていて、エンジニアは2,3人ずつくらいで固まって作業をしています。ビジネスサイドも開発サイドも混ざったワンフロアなので、静かすぎずうるさすぎず、少しガヤガヤしています。

エンジニアは業務委託の方を含めリモートでの作業が多いので、メッセージで伝わりづらいことはSlackのハドル機能(音声ミーティング)を使用しています。使用頻度があまりにも高いので「ハドれますか?」と「ハドル入ります」のスタンプが最近作成されました笑

終わりに

開発を進める中で、独学では意識していなかった点が顕著に現れていると感じました。命名の仕方やプルリクの出し方、未来の変更を想定した実装など、独学では得られなかった気づきと学びがたくさんあります。まだまだ至らぬ点が多いですが、来年の入社に向けて良いスタートダッシュが切られるよう頑張りたいと思います!

gRPC Server Streamingでメタ情報も一緒にレスポンスする

キャンプ場を検索・予約できるサービス(以下hinata-spot)の開発を担当している名嘉眞です。

hinata-spotではbackendにGoを、backend間の通信にgRPCを採用しています。

今回はgRPCのServer Streamingでメタデータも一緒にレスポンスする方法をブログにします。

どんな時に使うのか

あまり多くはないかもしれないですが、Server Streamingで通信したいユースケースで、かつそのユースケースで使うメタデータなどもレスポンスしたい場合です。

例えば、1つ1つが容量の大きなデータを複数レスポンスする必要があり、そのデータ総数などのメタデータも送信したい場合などになります。

より具体的な例で以下のようなユースケースで考えてみます。

  • 予約できるプラン(1プランで多くの項目の情報が設定されていると仮定)のリストを返す。

  • またそのプランの総数も必要になる。

上記のユースケースだと、proto定義は以下のようになるかも知れません。

rpc GetPlans(GetPlansRequest) returns (stream GetPlansResponse);

message GetPlansRequest {
  string check_in_date   = 1;
  string check_out_date  = 2;
}

message GetPlansResponse {
  Plan  plan       = 1;
  int64 total_size = 2;
}

message Plan {
  string id          = 1;  // ID
  string name        = 2;  // プラン名
  string description = 3;  // プラン詳細
  // そのほか多くのフィールドがあるとします
}

rpc GetPlansはServer Steamingで定義しています。レスポンスのGetPlansResponseには、Plan型とtotal_sizeが定義されています。 Plan型はstreamingで通信されるので複数回レスポンスされますが、その都度プラン総数もレスポンスすることになってしまいます。

また他のパターンだとプラン総数をレスポンスするrpcを定義するという方法も考えられます。 この場合クライアント側は2つのrpcをcallする必要があります。

rpc GetPlans(GetPlansRequest) returns (stream GetPlansResponse);

// プラン総数をレスポンスするrpc
rpc GetPlansCount(GetPlansRequest) returns (GetPlansCountResponse);

上記のような状況の場合oneofを使うと以下のように定義することがができます。

oneofには以下のように記載されています。

Oneofフィールドは、oneof内のすべてのフィールドがメモリを共有しており、同時に設定できるフィールドが1つだけであることを除けば、通常のフィールドと同じです。

つまりoneofを使って宣言したmessage型はその内部のうちどれか1つのフィールドを返すということを定義できます。 今回のユースケースでoneofを使ってみた場合が以下です。 oneof PlanInfoは、planかtotal_sizeのどちらかをレスポンスします。

rpc GetPlans(GetPlansRequest) returns (stream GetPlansResponse);

message GetPlansRequest {
  string check_in_date   = 1;
  string check_out_date  = 2;
}

message GetPlansResponse {
  oneof PlanInfo {
    Plan  plan       = 1;
    int64 total_size = 2;
  }
}

message Plan {
  string id          = 1;  // ID
  string name        = 2;  // プラン名
  string description = 3;  // プラン詳細
  // そのほか多くのフィールドがあるとします
}

gRPC Server側の実装は以下のようになります。 DBなどからプランを取得してきたあと、まずプラン総数をSendし、その後forでループしながらプランをSendします。

func (s *backendService) GetPlans(req *spot.GetPlansRequest, stream spot.BackendService_GetPlansServer) error {
    params := &usecase.ListSpotPlanParams{
        SpotID: req.GetSpotId(),
    }

  // DBなどからプランを取得してくると仮定
    plans, err := s.usecase.ListPlanUseCase.ListSpotPlan(stream.Context(), params)
    if err != nil {
        return handleError(err)
    }

  // プランの総数を先にSendする
    err = stream.Send(&spot.GetPlansResponse{
        PlanInfo: &spot.GetPlansResponse_TotalSize{
            TotalSize: int64(len(plans)),
        },
    })
    if err != nil {
        return handleError(err)
    }

    for _, plan := range plans {
        err = stream.Send(&spot.GetPlansResponse{
            PlanInfo: &spot.GetPlansResponse_Plan{
                Plan: s.convertPlan(plan), // protoで定義したPlan messageに変換してレスポンスに設定
            },
        })
        if err != nil {
            return handleError(err)
        }
    }

    return nil
}

実際のクライアント側でレスポンスを受け取るコードが以下のようになります。 まずプラン総数を取得します。

その後forでプランを取得していきます。

stream, err := backendService.GetPlans(ctx, &spot.GetPlansRequest{
  // 省略
})
if err != nil {
    return nil, err
}

resp, err := stream.Recv()
if err != nil {
    return nil, err
}

// 取得したプラン総数でスライスのcapを指定するにしている
res := make([]*model.Plan, 0, resp.GetTotalSize())

for {
    // 受信データ量が多いとgRPC転送量エラーになるので、streamで一件ずつ受け取る
    p, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        return nil, err
    }

    res = append(res, &model.Plan{
        Plan: p.GetPlan(),
    })
}

return res, nil

oneofを使うことで1回のrpc callで異なる種類のデータをそれぞれ取得することができます。 少しスマートな感じがしますね。 また、今後レスポンスにフィールド追加を行いたい場合でもoneof型で宣言したフィールドに追加することで解決できます。

最後に

gRPC Server Streamingでメタデータも一緒にレスポンスする方法について記載しました。誰かの役に立てば幸いです。

vivit では一緒に働くエンジニアを大募集しています 🎉

www.wantedly.com

少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております!