バッチ処理をEC2からFargateへ移行した

インフラエンジニアの佐藤です。
今回はEC2上で実行していたバッチ処理をコンテナ上で実行させるようにしたのでその話を書いていきます。
コンテナ化するにあたりマネージドサービスを活用してサーバレスに運用していきます。
利用したサービスや移行にあたっての問題、活用事例を紹介していきます。

背景

用途を分けて2台のバッチサーバで運用していましたが、Rubyのアップデートを始め各種ミドルウェアの更新をする度に、新しいAMIを作成してバッチサーバの入れ替え作業を実施していました。
AMIを作成する時間が長かったり、入れ替えに伴う工程が多かったりといくつかの負債がありコンテナを導入することで運用コストを下げようといった狙いからスタートしました。

まずは、入れ替え頻度が高めで運用コストがかかっているサーバロールを選定しました。
主に下記3つが対象となります。

  • ウェブサーバ
  • 非同期処理サーバ
  • バッチサーバ

比較的容易に変更できそうなバッチサーバのコンテナから実施することにしました。

利用したマネージドサービス

  • AWS Fargate
  • AWS Step Functions
  • AWS Lambda
  • CloudWatch Logs Insights

Fargate

バッチ処理を動作させるコンテナ基盤として利用しました。

Cloudwatch Events多重起動問題

タスクを定期実行できるスケジュール機能があり、Cloudwatch Eventsの仕組みが使われています。
こちらをCronの代替として利用することも可能ですが、同じタスクが複数回トリガーされる可能性があります。

バッチ処理では該当時間帯に一度の実行しか許容できない処理も存在するので、安易にこのスケジュール機能を使うことはできませんでした。
同じタスクの多重起動を回避するためタCron専用のサーバを用意することにしました。

※後日、Step Functionsを用いてこの問題を回避できることが分かったので、いずれはそちらに切り替えたいと思っています。
参考: 重複実行を許容しないステートマシンを構築

固定IP対策

EC2インスタンスに固定IPを付与していました。
特定のサーバと通信する際に利用していますが、Fargateタスクとして実行する場合にはランダムな可変IPが付与されてしまいます。

回避策としては下記のようなパターンなどが挙げられると思います。

  • NAT Gatewayを利用する
  • Proxyサーバを経由する
  • 可変IPを許容できる仕組みに切り替える

今回は可変IPを許容できる仕組みへの切り替えました。
固定IPに依存している処理は数が少なかったのでSecurityGroupを利用したり、処理の切り分けをして一部でのみ固定IPで通信するようにしました。
依存している処理が多ければNAT GatewayやProxy経由での通信が良さそうに思いますが、費用や運用コスト面を考えたところ可変IPに対応するようにしました。

Step FunctionsとLambda

コンテナの起動に失敗した際のワークフローを作成するために利用しました。

稀にFargateタスクの起動に失敗することがあります。
失敗したタスクのStopped Reasonを確認すると、下記のようなエラー例を確認することができます。

  • ResourceInitializationError: failed to configure ENI: failed to setup regular eni: context deadline exceeded
  • Timeout waiting for EphemeralStorage provisioning to complete.
  • Timeout waiting for network interface provisioning to complete.

これらの StopCode を確認してみると TaskFailedToStart となっており、タスクの起動自体に失敗していることがわかります。
公式リファレンスからも失敗時は手動対応か再試行の自動化が解決策に上がられているので、Step Functionsを利用した再試行の仕組みを導入しました。

ただStep Functions エラー処理を確認しても TaskFailedToStart のエラーはありません。
そのため、細かいエラー処理は独自のエラーハンドリングを作成することになります。

実装例

Lambda

Lambda Functionで補足したいエラー処理を実装しておきます。
Step FunctionsでRun Fargate Taskからの出力を変数に格納しておくことにより、Lambdaステップへの入力でその変数を使うことができるようになります。
この場合は result に格納しています。

class TaskFailedToStartException(Exception): pass

def error_handler(event, context):
    if event.get("result").get("StopCode") == "TaskFailedToStart":
        raise TaskFailedToStartException("TaskFailedToStart Error")

Step Functions

想定するワークフローを作成しておきます。 この例ではFargateタスクを実行し、その結果をハンドリングするといったフローになります。
Run Fargate Task での出力結果は成否に関わらず全て result に格納します。
result の内容から上記のLambda Functionでエラーを検出し、その結果から次のステップを選定しています。
再試行フローを実装するにはLambda を使用してループを反復するを参考にします。

"Run Fargate Task": {
  "Type": "Task",
  "Resource": "arn:aws:states:::ecs:runTask.sync",
  略
  "Catch": [
    {
      "ErrorEquals": [
        "States.ALL"
      ],
      "ResultPath": "$.result",
      "Next": "Error Handler"
    }
  ],
  "ResultPath": "$.result",
  "Next": "Error Handler"
},
"Error Handler": {
  "Type": "Task",
  略
  "Catch": [
    {
      "ErrorEquals": [
        "TaskFailedToStartException"
      ],
      "ResultPath": "$.result",
      "Next": "捕捉時の処理"
    }
  ],
  "ResultPath": "$.result",
  "Next": "次のステップへ"
},

CloudWatch Logs Insights

Fargate や Step Functions のログを分析用に利用しています。
既存バッチ処理ではEC2上にログを出力していましたが、コンテナ導入に伴い出力先をCloudWatch Logsに変更しました。
AWS上にログを溜め込むことにより一元管理することができたりログの集計がしやすくなります。

実行時間の長い処理などを一目で確認できるので処理の改善などに役立ちます。
CloudWatch Logs Insights を使用したログデータの分析には様々なログを解析できる仕組みが記載されています。

バッチ実行時間の集計の例

今まではEC2上に出力されていたログを確認していましたが、CloudWatch Logs上にログを集約することにより簡単に確認することができます。

下記はAWS Step Functionsの実行単位ごとに実行時間を集計してみた例となります。

サンプルクエリ

対象はStep Functionsから出力されるログです。
クエリ構文に使い方が記載されているので参考になります。

fields @timestamp, execution_arn, details.output
| parse details.output '{"commands":[*]' as commands
| filter details.output like "commands"
| stats earliest(@timestamp) as startTime, latest(@timestamp) as endTime by commands, execution_arn
| fields abs(endTime - startTime) / 1000 / 60 as minutes, datefloor(startTime, 10m) as execute_time
| display execute_time, commands, minutes, execution_arn
| sort minutes desc

execution_arnはStep Functionsの一意の実行単位となるので、execution_arnごとの一番早いタイムスタンプと一番遅いタイムスタンプの差分を出します。
差分はミリ秒になっているので分単位に変換し可読性を上げます。

まとめ

サービス本体基盤へコンテナの導入をしてみました。
サーバレス化していくためには様々なマネージドサービスを利用します。
それぞれ使い方に特徴があって対策や活用事例などを探すのは大変ですが、導入すると運用コストを削減することができるので引き続き改善していきたいと思います。

最後に

ジモティーではエンジニアを募集中です!
jmty.co.jp


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

データ設計と向き合う

サーバサイドエンジニアの坂根です。

最近某企業から販売されるウイスキーの価格改定が発表されましたね。

国産ウイスキーが転売されることなく、安定供給されることを切に願います。

さて、今回はデータ設計で気をつけていることについて話していきます。

何を大事にしているか

ヒトコトで言ってしまうと、現実に忠実であることです。

システムのデータと現実は、しばしば対になります。 そしてシステムの関心事が増えると、現実に沿ってデータを増やします。 現実に反するマジカルな設計になっていると、データを増やす際に歪な形で積み上げていくことしかできず、最終的にはジェンガのように崩れてしまうのです。

現実に反するマジカルな設計

では、マジカルな設計とは何か?具体例を基に見ていきましょう。

初期

資産管理システムを構築します。 どの社員に液晶モニタを貸与したか、一覧で表示できるようにしましょう。

マジカル設計の初期
マジカル設計の初期

中期

一部の社員からモニタが小さいというクレームがあり、大きめのモニタを導入することになりました。 資産としては別物なので、管理システムでも違いが分からなければなりません。

インチ数が分かればいいんだな!とストレートに付加します。

マジカル設計の中期
マジカル設計の中期

後期

資金が潤沢になり、業務効率を少しでも上げるために予備のモニタを購入できることになりました。 社員に貸し出さない予備資産を登録します。

社員にぶら下げることができなくなってしまったため、やむを得ず在庫として登録することを考えます。 しかし資産を一覧で可視化しようと思うと、社員と在庫の双方を集計しなければならなくなりました。

マジカル設計の後期
マジカル設計の後期

現実に忠実な設計

では、現実をできるだけ忠実に再現するとどのようになるでしょうか?

初期

社員に貸与した液晶モニタを、一覧で表示できるようにします。

現実に忠実な設計の初期
現実に忠実な設計の初期

中期

貸与したモニタのインチ数が区別できるようにします。

現実に忠実な設計の中期
現実に忠実な設計の中期

後期

在庫を登録できるようにします。

現実に忠実な設計の後期
現実に忠実な設計の後期

拡張性

後者の設計では、今後1人に貸与するモニタの数が複数に増えた場合でも無理なく対応できそうです。 それは、データが現実に通じており、似た土台から変化を起こすことができるからです。

つまり、拡張性高く保つためには、現実に忠実であることが重要なのです。

まとめ

検索効率を上げる、特定の何かに特化する、そのために現実を犠牲にすることもあるでしょう。 それでも私は、現実に忠実なモデルを起こした後、犠牲にするもの(リスク)を認識したうえで取捨選択します。

みなさんも良いデータ設計ライフを!

ジモティーではバックエンドに熱い魂を捧ぐ仲間を募集しています。 ご興味のある方はこちらを御覧ください。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

ジモティーのフロントエンドをNext.jsに移行していくという話

どうも鈴木です。好きなプレインズウォーカーは初代ガラクです。

最近スタンダードでは緑単が強かったみたいですが、新弾出てどうなるんでしょう。

それはさておき、今回はジモティーのフロントエンドをNext.jsに移行していくという話を書きます。

背景

ジモティーは今年でリリース後10年を迎え、その間、Webブラウザ版のジモティーはモノリシックなRailsアプリケーションとして開発を継続してきました。 当然ながら技術負債の蓄積はさけられず、特にフロントエンドの生産性の低下が顕著でした。

まず、以下の理由でフロントエンド単体で見た時に生産性が悪くなっていました。

  • デザインガイドライン整備前の無秩序なCSS
  • メンテナンス性の低いJavaScriptコード
    • 一定以上の複雑さを持つ機能が無理矢理jQueryで書かれている
    • 一部Vue.jsを採用しているがRailsのview上に実装するためにベターjQuery的な活用に留まっている

また、Railsエンジニアがフロントエンドからバックエンドの実装を担当するため、以下の問題がありました。

  • フロントエンドとバックエンドが密結合しているため分業がしづらく、並行開発できない=リードタイムが長い
  • チームとしてフロントエンドの専門的なスキルが伸びにくい

ということで、これらの負を解決し、以下を実現することを目的にフロントエンドの刷新に着手しました。

  • フロントエンドエンジニアとバックエンドエンジニアが分担してそれぞれの領域に集中しやすくする
  • 技術負債になっていたコードを置き換え、モダンなフロントエンド技術を採用することで、フロントエンド単体での開発生産性を上げる

制約

さて、やると決めたのは良いものの、ジモティーのサービスの特性を考えた時にいくつかの制約がありました。

  • SEO上重要なページはSSR(Server Side Rendering)で実装する
  • 既存ページを移行する際、UX・パス・機能は基本的に維持する
    • (開発生産性のための再構築なので、UXの大幅変更により事業KPIに影響がでるのを避けるため)

これらを踏まえた上で技術選定しました。

技術選定

選択の理由も添えて紹介します。

  • TypeScript: 採用
    • 型安全性の恩恵を受けたいので採用
  • React or Vue.js: Reactを採用
    • 現時点でのTypeScriptの親和性を考えるとReactが優勢と判断
    • 検討時、Vue 3が出てから間がなく知見が溜まっていないということも考慮した
  • フロントエンドフレームワーク: Next.jsを採用
    • SEO上重要なページでSSRを採用するためSSR可能なフレームワークは必須
  • UIフレームワーク: 不採用
    • 新規サービスのこちら ではMaterial UIを採用していたがジモティーでは独自のデザインシステムがあるため既製のCSSフレームワークを導入するとカスタマイズの工数がかえってかさむと判断。
  • BFF(Backend For Frontend): 不採用
    • 今回の目的に照らすと必須ではない
    • 通信速度の悪化や運用・保守コストの増大などのデメリットが見込まれるため不採用
    • BFF で裏側のRESTを束ねて、GraphQL + Apollo Clientでハッピーになるんや、みたいな話もあったけど一旦冷静になった
  • StoryBook: 採用
    • デザイナーとエンジニアのコミュニケーションがスムーズになる
    • 長期的にメンテナンスするような自社サービスなら必須レベルと判断

移行の課題

ジモティーの場合、大量の既存ページがありそれらを一気にNext.jsに移行することは現実的ではないため、ページ単位あるいはまとまった機能単位で移行していきます。 したがって、移行済のページはNext.js、移行前のページとAPIコールはRailsにリクエストを振り開ける必要があります。

そこで問題になるのは以下の点です。

  • リクエストの分配方法
  • ログイン認証
  • CSRF対策

リクエストの分配方法

以下の方法を検討しました。

  1. Next.jsとRailsでドメインを分ける
  2. Nginxをリバースプロキシとして使って振り分ける
  3. ALB(Application Load Balancer)のパスベースのルーティングで振り分ける

ドメインを分ける方法の場合、移行前後でページのURLが変わるデメリットが大きいと考え不採用にしました。

Nginxをリバースプロキシとして運用する場合、スケーリングやメンテナンスの対象となるサーバーが一種類増えるという問題があります。

一方、ALBについては既に使っているため管理対象が増えないという状況でした。

したがって、ALBのパスベースのルーティングを使って、Next.jsとRailsへの振り分けを行うことにしました。

ログイン認証

ブラウザからのリクエスト先がRailsの場合とNext.jsの場合ある、という前提でログイン状態を維持する必要があります。

この問題はSPAのログイン認証をどう実装するか、という問題とよく似ており、 こちらの 記事 が非常に参考になりました。

今回の場合は、Next(フロント)とRails(API)が同一Originのため、参考記事内の「Sessionを用い、SessionのCookieはSameSiteCookie」を採用することができます。

あとはNext.jsからRailsにAPIコールする箇所でクライアントから受け取ったCookieを引き渡すように実装すれば、Next.jsページとRailsページ(API)の間でログイン状態を維持することができます。

※図を挿入

CSRF対策

RailsではフォームにCSRFトークンを埋め込み、セッションに保持しているトークンと同一か検証することでCSRF対策を行っています。

一方、Next.js移行後の画面ではフォームをNext.jsが提供することになります。Railsが発行するCSRFトークンをNext.jsのフォームに埋め込めば従来の方法で検証が可能ですが、フロント〜Rails間でのCSRFトークンの通信が発生するので通信のオーバーヘッドを考慮すると好ましくありません。

そこで、以下の2つの対策を組み合わせて実施することにしました。

  1. SameSite Cookie
  2. Originヘッダの検証

SameSite Cookieとはクッキーに指定できる属性の一つです。SameSite=Laxを指定するとブラウザは別サイトからのPOSTの際にCookieを送信しません。これによってCSRFを防ぐことができます。

2021年時点ではモダンなブラウザでSameSite=Laxがデフォルトになっていますが、まだ未対応のブラウザもあり得るため明示的に指定します。

Rails.application.config.session_store :cookie_store, same_site: :lax

Originヘッダの検証

古いブラウザでSameSite属性をサポートしていないものが存在するため、Rails側でのOriginヘッダの検証も実施します。

ALLOWED_ORIGIN = "https://example.com"

def allowed_origin?
  origin = request.headers["origin"]

  # 同一Originの場合にOriginヘッダを付与しない挙動のブラウザもあるため、空の場合は許可する
  return true if origin.blank?

  ALLOWED_ORIGIN == origin
end

We are hiring !

ジモティーではこの記事で紹介しているフロントエンドの刷新をはじめ、様々な挑戦のために新しい仲間を求めています! ご興味のある方はこちらを御覧ください。

jmty.co.jp


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

リリース作業をかんたんに! git-pr-release + Google Apps Script + Ruby スクリプト + GitHub Actions によるリリース作業改善の取り組み

ジモティーでサーバサイドとインフラを担当している吉田です。

前回は ISUCON10 に参加した話を投稿しました。今年もちょうど ISUCON11 を終えたばかりですが、残念ながら予選敗退となりました。ISUCON への参加はウェブエンジニアとしてのキャリアを見つめ直すとてもよい機会で、来年こそは決勝に進みたいと思っています。

さて、今回はジモティーのサーバサイドで1年ほど運用している、リリース作業の一部自動化の取り組みについて、背景から実施までをご紹介します。

従来のリリースフローとその課題

まず、弊社サーバサイドのリリースフローについて、当時の概要は次の様になっていました*1

変更前のリリースフロー
変更前のリリースフロー

  1. コードレビューを経て、トピックブランチを master ブランチにマージ(作業者: 各機能担当者
  2. リリース PR を作成(作業者: リリース担当者
  3. 機能担当者にリリース前テスト環境での動作確認依頼(作業者: リリース担当者
  4. 動作確認(作業者: 各機能担当者
  5. Jenkins のジョブを実行してリリース(作業者: リリース担当者

リリース担当者はリリースしたいメンバーによる立候補制で、サーバサイドのメンバーであれば誰でもこの役割になる可能性があります*2

この作業の1番の課題は、マージされた PR の「タイトル」「作成者」を目視で抜き出す事が大変な点です(図の2. および 3. に該当)。

詳細な流れは次の内容になります。

リリースPRの作成と動作確認依頼
リリースPRの作成と動作確認依頼

  1. master ブランチへのコミットログの中からマージコミット (メッセージが Merge pull request #xxxx from jmty/xxxx) を確認
  2. マージコミットの PR から「タイトル」「作成者」を確認
  3. 確認した「タイトル」をリリース PR の本文に入力
  4. 確認した「作成者」にリリース前テスト環境での動作確認を Slack のメンションで依頼

また、PR の「作成者」は GitHub のアカウント名なので、Slack のアカウント名に変換して Slack で本人にメンションする必要があります。そのため、たまに誤って違うメンバーにメンションをしてしまう問題も発生していました。

変換ミスによる誤メンション
変換ミスによる誤メンション

これをマージ済み PR の数だけ繰り返す必要があり、リリースタイミングによっては1度に10件を超える事もあります。

改善

事前準備

まず、課題を明確にします。こちらは前述の通りです。

  • リリース PR 作成時に、マージされた PR の「タイトル」「作成者」を目視で抜き出す事が大変
  • 誤って違うメンバーに動作確認依頼を行ってしまう

次に、その解決方法を調べます。

基本的に手作業で行っていた、マージされた PR の「タイトル」「作成者」を目視で抜き出して作業担当者に依頼する作業を自動化する方針で、かんたんな実装を行いました。これは、次で行う提案のための準備になります。フィードバックの結果、内容を大きく変更する可能性も十分あるため、あまり作り込まず、かつ GO サインをもらえるレベルにします。

KPT での提案

準備した内容をもとに、週1で実施しているウェブチームの KPT で下記の提案をしました。

  • 課題の共有
    • リリース作業がツラい
  • 解決案の提示
  • FAQ
    • リリースフローはどう変わるのか
    • 導入・運用コスト
    • メリットだけではなくデメリットも
    • なぜ CI は既存で利用している CircleCI ではなく GitHub Actions で動作させるのか、など

想定していた実装工数は比較的小さいため、一気に導入する手もありますが、次の観点から提案のステップを挟むことにしました。

  • 共通の課題である事の確認および、ニーズの確認
  • 異動や退職などで技術的な負債となる可能性がある
  • リリースフローの変更により、障害に繋がりやすくなる可能性がある

結果はおおむね期待していた良い反応だったため、具体的な実装に移りました。

変更後

最終的に次の流れになっています。

変更後のリリースフロー
変更後のリリースフロー

  1. コードレビューを経て、トピックブランチを master ブランチにマージ(作業者: 各機能担当者
  2. リリース PR を作成(作業者: リリース担当者
  3. 機能担当者にリリース前テスト環境での動作確認依頼
    1. 本文を自動更新 (git-pr-relase gem)
    2. GitHub と Slack ID のマッピングを取得 (Apps Script)
    3. 動作確認依頼 (Ruby スクリプト)
  4. 動作確認(作業者: 各機能担当者
  5. Jenkins のジョブを実行してリリース(作業者: リリース担当者

「機能担当者にリリース前テスト環境での動作確認依頼」の作業を自動化しています。

それぞれの技術トピックについてご説明します。

git-pr-release

git-pr-release はリリース PR を作成・更新する gem です。マージされた PR からタイトルや担当者を取得して、PR を作成・更新してくれます。

当初自作をしようと考えていたところ、希望の機能を満たすこちらの gem が見つかり、OSS かつ定期的にメンテナンスされていたため採用しました。

最近 GitHub の仕様変更があり、期待する動作をしなくなる事がありましたが、すぐに対応されたものがリリースされました。

もし自作していた場合、同様の対応を行う可能性が高かったはずです。

Google Apps Script

GitHub アカウント名と Slack ID のマッピングを JOSN として返すウェブアプリケーションサービスです。

次はサービスのURLに対して curl からの擬似的なアクセス例になります。

$ curl -H "Authorization: token xxxx" -L \
  --dump-header - \
  "https://script.google.com/xxxxxx"
(省略)
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
(省略)

[
  {
    "slack_member_id"=>"XXXX",
    "slack_display_name"=>"kyoshida", 
    "github_login_name"=>"kyoshidajp"
  },
  {
    "slack_member_id"=>"YYYY",
    "slack_display_name"=>"yyyy",
    "github_login_name"=>"zzzz"
  },
  ...
]

マージされた PR からその作成者(機能担当者)のアカウント名を取得した後、Slack ID に変換する際にこのマッピング情報を参照します。

事前に Google スプレッドシートにそれぞれの対応を記載しておきます。新しいメンバーが加入したら都度こちらに追加します。このシートのメンテナンスが手動になってしまいますが、他の利用用途でも使えるようになります。

Ruby スクリプト

リリース PR の本文から、マージされたPRの「タイトル」「担当者」を取得して Slack に通知します。

Ruby は弊社サーバサイドにてメインで利用しているプログラミング言語です。ユニットテストや JSON の変換など、豊富なライブラリが付属している事に加えて、Slack や GitHub 連携など16万を超える*3サードパーティのライブラリによる素晴らしいエコシステムがあります。

同様の目的には、後述する GitHub Actions のワークフローコマンドから gh や jq を駆使して行うことも可能だと思いますが、上記観点から Ruby スクリプトとしました。

GitHub Actions

git-pr-release および、Slack に通知する Ruby スクリプトの実行環境です。

master から release ブランチへマージするリリース PR が作成されたタイミング、もしくはmaster ブランチにコミットが行われたタイミングで実行されます。後者については、リリース PR が作成された後に、追加でリリース機能を含めるケースに対応するためです。

CI 環境としてすでに利用している CicleCI ではなく、GitHub Actions を採用したのは次の理由によるものです。

  • 世の中の流れとして GitHub Actions がかなり普及してきており、キャッチアップをしておきたい
  • 想定の利用ケースでは無料枠に十分収まる
  • 面白そう

結果

改善により、当初の課題であった、リリース担当者による目視かつ手作業での依頼が不要になりました。

自動で通知される動作確認依頼
自動で通知される動作確認依頼

また、誤って別のメンバーに確認依頼してしまう問題も発生していません。

さらに、工夫したポイントとして、デプロイ時に Slack で通知されるリリースノートの内容が分かりづらかったため、同様の仕組みで整理しました。

整理されたリリースノートの通知
整理されたリリースノートの通知

この結果、エンジニアだけでなく、CS を含む社内メンバーは機能概要機能担当者リリース担当者が明確になりました。

まとめ

日常で感じた課題の解消をモチベーションに、その改善の提案、そして実施までをご紹介しました。

自動化した作業は、以前は1回あたりおよそ5〜15分程度だったので、リターンを時間で換算するとどうしても投資効果を低く感じてしまいがちです。しかし、日々課題に感じていて改善の目処が立っているのであれば、積極的に取り組むべきだと思います。

そもそも、対象の作業は手作業で日々繰り返され、自動化が可能なものでした。こういった、いわゆるトイルの撲滅は(インフラ)エンジニアが価値を発揮しやすい領域ではないでしょうか?

少なくとも、以前の状態には戻りたくないのが率直な気持ちです。

宣伝

ジモティーではエンジニアを募集中です。

jmty.co.jp

日々の不満や課題を一緒に解決していきましょう!


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

*1:master ブランチへのマージ後にリリース前テスト環境への自動反映など、細かい内容は省略

*2:これは執筆時点も同じ

*3:執筆時点の https://rubygems.org/stats より

ジモティーのフロントエンド事情

初めまして。

2020年末からジモティーでフロントエンドエンジニアとして開発している山口です。

今回はジモティーのフロントエンド事情について紹介します。

ジモティーフロントエンドの現状

現在のジモティーのフロントエンドはRailsのAction Viewを用いて

  • jQuery で Javascript 周り(一部のページの一部の DOM に対して jQuery 代替として Vueも利用)
  • Rails の decorator でフォーマットして Haml でマークアップ
  • Sass でスタイル付け

といった構成になっています。

しかしサービスの規模が大きくなってきたこともあり、どのidやclassをどの jQuery コードが参照しているのか追いづらかったり、 似たスタイルを持つ似た名前のclassが乱立してしまいどれを利用すべきなのかの見通しがつかないなど、開発が辛くなってきているのが正直なところです。

Railsの開発チームはアプリ向けのAPIの作成やバックエンドの開発と並行してフロントエンド開発も行っているため、 辛さが出てきた現状が会社として求める開発スピードUPの足枷になってきています。

新規プロジェクトにてReact(NextJS)を利用

今月リリースした新規プロジェクト、自治体向けの電子申請・申請承認システム「LOCONET(ロコネット)」にて、弊社としては初めて React を利用した開発体制を敷きました。

バックエンドはジモティーに引き続きReilsを利用し、フロントエンドはバックエンドとは完全に分離してReact(NextJS)+Typescript で作成、さらにOpenAPIによる APIドキュメント整備とStorybook によるコンポーネントのドキュメント整備を行うことでバックエンド開発者やデザイナーとのコミュニケーションを取りやすい環境を構築しました。

技術負債との比較

前述した現在のジモティーの開発状況と比べると今回の開発体制では

  • styled-compomentsを利用でスタイルのスコープを制限してスタイルの見通しが付きやすくなる
  • contextの利用を避けてあえてpropによるバケツリレーを選択することでstateなど変数の変更が追いやすくやる
  • Typescript によりコードがよりドキュメントとして理解しやすい形で残るようになったため、第三者がコードを追いやすくなる
  • バックエンドと分離されることでアプリチームと似た分担で開発が可能になり、適材適所の分担で開発が行えるようになる
  • デザイナーとパーツ単位で実装しているスタイルを見ながら密なコミュニケーションが取れるようになる

などのメリットを得ることができました。

代わりに

  • Typescript 導入により、追加で工数が発生してしまう
  • 保守していかなかればならないものが増えた(OpenAPI,StoryBook)

といったデメリットは発生しています。

Typescript導入において型管理以外に特に時間を要したのは次に紹介する2項目です。

外部ライブラリ利用時の型調査の時間

外部ライブラリ使用した際にライブラリ内に定義されている型の内容、利用方法の確認に時間を要しました。

特に初めて利用するライブラリの場合、javascript時の倍は時間がかかる感覚です。

直で呼び出す場合より子コンポーネントへライブラリの一部を受け渡しする必要があった場合など子コンポーネントのpropの定義に悩むことが多かったです。(大抵公式のドキュメントにその場合の型の定義方法が書いてないので直接ライブラリを追ってみるしかなかったです)

例) React Hook Formの場合 https://react-hook-form.com/jp/get-started/

const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>()

useFormで呼び出してきた各種メソッドregister, handleSubmit, watch, errors をコンポーネントへ受け渡したい時、どのような型で待ち受けるべきかがドキュメントからはわからず、なるべくanyなどを使わず実装しようとすると最適解を見つけるのに時間がかりました。

型推論のための追加記述

頻出するわけではないですが、遭遇した場合に解決の時間を用意したのがこちら。

適切な型推論が行われるようにするために javascriptだけならそのままで要件を満たして動作するコードに対して - typescriptが型推論に対応しているメソッドに書き直す - 自前で型推論させる処理を追加する(javascriptの動作上は必要のないコード) という対応をする必要が発生しました。

例) filterを利用してarrayを整理しても方が絞り込まれない例

const x = [123, null] // 型推論結果 const x: (number | null)[]
const y = x.filter(v => v !== null)
console.log(y) // [123]
// 型推論結果 const y: (number | null)[]
// number[] にならない

このままではビルドが通らないので型ガードを駆使したりして型を絞り込む必要が出てきますがチームへのTSの知見が溜まっていないと解決まで時間がかかってしまいます。

...

しかしながら、 Typescript導入やOpenAPI、Storybookはある程度のコード規模が大きくなり、チームの人数が増えてきたときに効力を発揮する要素なので投資として受け入れるべきコストだと考えています。

VueとReactの違い

実は私はジモティージョイン前までVueをフロントエンド開発に利用していたため今回のプロジェクトが React に触れる初の機会でした。

そのため Vue 開発と比べた時の違いについてもいくつか感じることができましたので紹介します。

自由度が高く、良くするも悪くするも開発者次第なReact vs 形式がしっかりしており、とっつきやすいVue

Vue はコンポーネント内で何をどこに定義すべきかはっきりしているのに比べて、React はとにかく自由度が高いため、開発者がやりたいように柔軟な書き方ができる反面、選択肢が多いため、知見がない状態ではベストプラクティスを見出すのが難しいと感じました。

コンポーネントファイル内でstateやeventなどを定義する場所(Vueはdataやwatch,methodなど書き場所が固定されていて開発者でバラつかない)や、スタイルの定義方法は他のオープンソースの事例をいくつか参照してみましたがどれもバラバラで自分たちに合ったやり方を見出す必要がありました。

スタイル付けに関してはスコープをコンポーネント内に留めたいのと、なるべくcssそのままの形で記載ができることをとってstyped-componentをチョイスしましたが正直Vueの方がシンプルにcssそのまま埋め込め、sassなども導入しやすかったので扱いやすかった印象です。

Typescriptとの親和が進んでいるReact、やれることが多いNextJS

自分がしっかりTypescriptに触れていなかった原因でもあるのですが、どうしてもVueの方がTypescript導入が遅れているので、きれいに導入できるReactの方が今のところは軍配が上がるという印象でした。

また、NextJSの方がISRなどへの対応が進んでおり、よりサービスに適した形でビルド方法を検討することができる点も好印象でした。

今後のフロントエンド開発

今後ジモティー自体でも Reils の MVC モデルに乗っかって jQuery を利用している現環境をやめて、今回 LOCONET で作ったようなフロントを分離した開発体制へ移行していく予定です。

かなり機能が多いサービスになっているため、大掛かりなプロジェクトにはなりますが、移行後はより良いユーザー体験と開発スピードを実現できると思います。

ジモティーは今回の私のように未経験技術へ挑戦する機会を積極的に与えてくださり 新規技術への興味関心度合いも高く、エンジニアとして働くのが非常に面白い組織です。

今後もこのサイトではジモティーのフロントエンド事情についての記事を投稿予定ですのでご期待ください。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

ジモティーのUI/UXに関する取り組みについて🧸

ジモティーエンジニア紅一点のnaruです🧸
iOSチームで開発を行っています。

最近は自粛で気軽に外に行けないこともあり、自分の所有する車を擬人化したりなどしておうち時間を過ごしたりしてます🧸

前回は↓こんな記事を書いております。 jmty-tech.hatenablog.com

今回もデザイン関連についての話をしたく、UI/UXに関して自社で行っている取り組みの一部を紹介しようと思います。

まず、UI/UXに関するコミュニケーションは今年に入ってからより活性化するようになりました。

というのも、
会社全体のチーム編成や考え方の見直しや、
ジモティーの根幹のDesignSystemの見直しについてデザイナーエンジニア間でMTGする機会が増えたこと、
エンジニア側からもUI/UXの提案を行うようになったことで、
よりお互いが互いの分野に歩み寄って一緒に学んでいきたいという思いが強くなってきた部分があるのかなと思います🤗
(DesignSystemについては、絶賛検討が進んでおりますので、然るべきタイミングでまた話ができれば😌)


#design_infoというオープンチャンネルを作り、


他社の事例を紹介したり、


デザイナーがプログラムについて調べてみたり、


新しいOSの変更点などを共有したりしています🌟

そして、今回メインで紹介したいのが、「UIパッション会」という取り組みです💪

UIパッション会

「もっと色々話したいよ〜🥺」というところで
名前の通り、「UIにもっと目を向けていこうぜ!意見交換しようぜ!💥」という名目の共有会を月1で有志を集い開催しています。

もともと自分は月1で他社アプリのアップデートの内容を共有する、
ということをやっていたのですが

もっと具体的に詳しく話し合ったり、

デザイナーから「こういうの実装したいんだけどどうかなあ」などをフランクに聞いてもらったり


ゴールは案件化ですが、あくまでもざっくばらんに話すを目的にしています:)


これまで実際話された内容としては、

「xxxみたいなUIって難しい?🤔」
「xxx画面の改善を考えているんだけど....🥺」

「某アプリ考察した!😀」

「ios14からAppleが提供するようになったこんな機能があるんだ😏」

あとは、
「〜〜〜っていう機能を作りたいからちょっとざっくり感想教えて欲しい🤗」

などなど....



実際ここから案件起案があがったりもあり、
新しい視点での発見や学びもあり良い取り組みだというところで今後も継続して実施できればと思います。

その他の取り組み

カスタマージャーニーワークショップ

有志で集まってはじめてのカスタマージャーニーマップワークショップという本を参考に、自社サービスについて分析。 各画面の優先度や既存機能の重要さ、改善ポイントをユーザー視点で考えるきっかけになりました💪

UXきんにくん

ディレクターやUXチームを中心に、チームに分かれて他社アプリを洞察して発表し合う、なども行っています。

おわりに

ジモティーでは上記のような取り組み以外にも 色々な分野でユーザー視点での考えを推奨し、評価される制度となっています。
今回書いた内容に興味をもっていただけたり、共感いただけたら幸いです。

jmty.co.jp


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

AndroidのローカルDBをSQLiteからRoomに置き換えてみた

はじめに

はじめまして。ジモティーに2021年1月からAndroidアプリエンジニアとしてい働いている谷です。

今回はAndroidアプリエンジニアとしてローカルDBをSQLiteからRoomに置き換えた話をさせていただければと思います。

Roomとは

置き換えの話に入る前にさらっとRoomについて説明したいと思います。 RoomとはGoogleが推奨しているSQLiteのORMです。 SQLiteHelperの作成やCursorの操作などめんどくさい処理を一手に引き受けてくれます。

実装も非常にわかりやすく、3つのコンポーネントを作成するだけでローカルDBの構築が可能です。

  • Entity
  • Dao
  • Database

合わせて非同期処理としてRxやKotlin Coroutinesなどのサポートもされており、非常に使いやすいです。

さらにデバッグツールとしてAndroid Studio 4.1から追加されたDatabase Inspectorというものがあり、APIレベル26以上の端末でアプリを実行している時に使用できます。 作成したテーブルの中身をSQLを使って操作したり、Database Inspectorのウィンドウでそのまま書き換えたりといったことが可能です。 こちらにどんな感じのものか詳しく書かれている記事がありますので、是非見てください。

そして以下簡易的ではありますがRoomの実装例です。

まずはEntityを作ります。 これがテーブルの役割を果たします。

 @Entity(tableName = "tbl_todos")
 data class Todo(
     @PrimaryKey(autoGenerate = true)
     val id: Long,
     val title: String,
     val isActive: Boolean
 )

続いてDaoを作成します。 データを取得したい場合はこちらに定義したメソッドを呼び出すことになります。

 @Dao
 interface TodoDao {
     @Insert
     suspend fun insertTodo(todo: Todo)
 
     @Query("SELECT id, title, is_done FROM tbl_todos")
     suspend fun getAll(): List<Todo>
 
     @Query("SELECT id, title, is_done FROM tbl_todos WHERE is_done = :isDone")
     suspend fun getDoneTodo(isDone: Boolean): List<Todo>
 
     @Update
     suspend fun updateTodo(todo: Todo)
 
     @Delete
     suspend fun deleteTodo(todo: Todo)
 }

最後にDBを実装します。

 @Database(entities = [Todo::class], version = 1, exportSchema = false)
 abstract class TodoDatabase : RoomDatabase() {
     abstract fun getTodoDao(): TodoDao
 
     companion object {
         private const val DATABASE_NAME = "todo-db"
 
         fun createTodoDatabase(context: Context): TodoDatabase {
             return Room
                 .databaseBuilder(
                     context.applicationContext,
                     TodoDatabase::class.java,
                     DATABASE_NAME
                 ).build()
         }
     }
 }

おそらく基本的にはDaggerなどでインジェクションすると思いますが、以下のように呼び出すことが可能です。

 // Daoの取得
 val todoDao = TodoDatabase.createTodoDatabase(context).getTodoDao()
 
 // 取得したDaoから各種メソッドでローカルDBにアクセスする
 todoDao.insertTodo(Todo("保存したいTodoのタイトル"))

ここまでが基本的なRoomの使い方となります。

背景

元々ジモティーのAndroidアプリではローカルDBとしてSQLiteとRealmが採用されておりました。 ジモティー内での使い分けとしては以下のような感じです。

  • SQLite
    • サーバーから取得したマスターデータをキャッシュするために使用。
  • Realm
    • 検索結果の履歴保存などに使用。

上記の内、SQLite部分で

  • 実装されているコードが古い。
  • 可読性が低くマイグレーションなどが発生した際にメンテナンスしづらい。
  • ローカルDBへの接続処理がUIスレッド上で実装されている。

という負があり、その辺りが解消されるのではないかということで導入することにしました。

以下、置き換え実施時に参考にした情報です。

  • Migrate from SQLite to Room
    • SQLiteからRoomへのマイグレーション方法について基本的なことが書かれており非常に分かりやすかったです。
  • Incrementally migrate from SQLite to Room
    • 後述するのですが、今回は完全な置き換えができなかったので段階的に置き換えをするにはどうしたら良いかが書かれておりこちらも非常に有用でした。

結果

結果は完全な置き換えはできず、段階的な置き換えを実施する形になりました。 修正範囲が当初想定していたよりも広くなってしまったのと、アーキテクチャに則って作られていない部分の改修難易度が高く時間的にも厳しかったためこの判断になりました。

完全な置き換えはできなかったのですが、最終的には以下が実施できました。

  • 既存テーブルをRoomのEntityとして再定義
    • Stringの配列で設定されていた既存のテーブルがKotlinのdata classとなったことで可読性が上がりました。
  • SQLiteを呼び出していた箇所の一部RoomのDao化
    • Dao内に定義しているQueryアノテーションでSQLが書けるので何をしているのかすぐに把握できるようになった。
    • さらにRoomには決まった形で使われる挿入や更新、削除はコンビニエンスクエリとして事前に定義されたアノテーションが存在するためSQLを書く手間が省け、小さいですが実装時の工数削減になりました。
    • RoomのDaoに置き換えられた箇所では、ジモティーではRxをまだ使用しているのでSingleを返却するようにでき非同期実行できるようになりました。(アプリ全体としてKotlin Coroutinesへの移行を行っているのでどこかのタイミングでsuspend functionに変更する予定です。)
  • SQLiteからRoomへの置き換え時に発生するマイグレーション処理の追加
    • Migrationを使うことで各スキーマバージョン単位でマイグレーション処理を記述することができるため、処理が追いやすく、メンテナンスしやすい状態になりました。
  • SQLiteOpenHelperをSupportSQLiteOpenHelperに置き換え
    • これにより既存のSQLiteの実装を壊すことなく、Room側のSQLiteに移行することができ、今後他の箇所のDao移行も行えるようになりました。

まとめ

今回移行してみて、置き換えたことにより非常に可読性が高く、UIスレッドをブロックしないように作れるためパフォーマンスも良いアプリになっていくだろうと思っております。

今回置き換えを行っていて、途中で方針転換をしないといけなくなったり、リリース直後にマイグレーション周りで不具合を出してしまったりと進め方として改善すべき点も見えてきた経験でした。

今後はSQLiteの処理からRoomのDaoに置き換えることができていない部分がまだまだあるので、そちらも随時進めて行きたいです。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです