データ基盤を支える技術 - ETLフレームワークの実践的な選び方・組み合わせ方

JX通信社シニア・エンジニア兼データ基盤担当大臣の@shinyorke(しんよーく)です.

最近やった「ちょっとした贅沢」は「休日, 自宅で🍺片手に野球を見ながらUberEatsで注文したランチを楽しむ」です. ⚾と飲食を提供してくださる皆さまに心から感謝しております🙏

JX通信社では,

  • 機械学習を用いたプロダクト開発・施策
  • プロダクト・サービスの改善に関する分析
  • 日々のイベントをメトリクス化して可視化(いわゆるBI的なもの)

を円滑かつ効率よく行うため, 昨年からデータ基盤を整備・運用しており, 現在では社員のみならず(スーパー優秀な)インターンの皆さまと一緒に活用し, 成果を出し始めています.

ainow.ai

なぜデータ基盤が必要か?どういった事をしているのか?...は上記のインタビューに譲るとして, このエントリーでは「データ基盤を支える技術 - ETL編」と称しまして,

  • Python製ETLをどうやって選ぶか?
  • JX通信社におけるETLフレームワークの使い分け
  • 小さく作って運用するETLならprefect最高やで!

というお話をさせてもらえればと思います.*1

TL;DR

  • ETLフレームワークはどれも癖があって得意不得意も違うので, 一つに絞らず適材適所にやってこ.
  • JX通信社では, 大きめのWorkflowを運用するのにAirflow(Cloud Composer), 1〜複数個のコンテナで収まるバッチをprefectでやってます.
  • 大きめWorkflow/小さいETL両方行ける(と思われる)prefectに期待する未来はあって良さそう

おしながき

対象読者

そこそこハイコンテクストな話題なので一応書きます.

  • 規模の大小を問わず, 企業および何かしらの団体でデータ基盤構築・運用に携わっている方.
  • AirflowやLuigi, kubeflowといったETLフレームワークを使った構築・運用をしたことがある方. SRE的なレイヤーが得意ならなお望ましい.
  • ETLとかWorkflowとかDAGとかそのへんの単語を解説しなくても読める方.
  • 「PythonicなETL話」なので, DigDag等, 別言語で実装・定義されたETLとの比較はしておりません.

Python製ツールの話メインですが言うほどPython出てこないです&統計・機械学習のアルゴリズム等は出てきませんのでそっち方面の知識は不要です.

PythonicなETLフレームワークの選び方

私の話で恐縮ですが, 過去のお仕事および個人プロジェクトでAirflowおよびLuigiを活用してきました.

上記以外にもJX通信社の仕事をはじめ結構色々とETLを構築・運用しましたが, ETLフレームワークの選び方として以下の3つが大事なのでは?と気が付きました.

  • Workflow(複数のタスク処理)実行に必要なプロセス(コンテナ)の数)
  • CPU・メモリの利用量が変動するか
  • 開発のしやすさ

構築するETLの要件・規模(と開発者の好みなどなど)を上記の3つの視点で組み合わせるといい感じになります!というのがこの先のお話になります.

Workflow(複数のタスク処理)実行に必要なプロセス(コンテナ)の数

結論から言うと,

Dockerのコンテナ1個で処理が終わるか, それとも複数のコンテナ・プロセスで処理するかで採用するフレームワーク変わるやで!

ということです.

データ基盤のバックエンドのタスクは,

  1. 欲しいデータを取ってくる「Extract」タスク
  2. 前処理・クレンジング・集計etc...何かしらの変換を行う「Transform」タスク
  3. DWH, ストレージなど必要としている所にデータを読み込む「Load」タスク

この3つの組み合わせで構成され*4, 大抵の場合「Extract -> Transform -> Load」の順で「Workflow」バッチを組むことになります.

これらの設計を進めると,

  • 一つのDBテーブル, 一つのアプリケーションログを相手にする比較的小さいETL Workflow
  • 複数のデータセット, 複雑な集計処理を伴うかつ, 別のWorkflowと依存する大きめのETL Workflow

に別れます. そして大抵の場合,

  • 「小さいETL Workflow」は一つのアプリケーションで完結することが多い. 例えば「RDBにselectした結果をS3に保存」みたいな小さなタスクはEmbulkのプロセス一つ立てればそれで終わる.
  • 「大きめのETL Workflow」は「AのWorkflowが終わったらBのWorkflowを」的な複雑な順序および障害発生時の冪等性を担保するような大掛かりな仕組みが必要となり, 仕組み的にもWorkflowが担うSLA的にもゴツい仕組みが必要.

となります.

小さいものはDockerコンテナ1個で収まるとかが多いですが, 大きいものはk8sクラスタ一個まるごと使う的な事が多く, ETLフレームワークもそれらによって得意不得意が異なるのでここのポイントは結構大きいんじゃないかなと思っています.

CPU・メモリの利用量が変動するか否か

Workflowが使うComputing Resources, 特にCPUとメモリの利用量が変動するか否かも大切なポイントとなります(機械学習系だとここにGPUも加わることになります).

これも一言でいうと,

処理に必要なCPU・メモリが一定量ならDocker Containerもしくは関数系のサービスでやる, 変動するならフルマネージドなクラウドサービス使うとかしよう!

です.

「1日一回, マスターデータをS3にダンプする」「アプリケーションログを1時間に一度BigQueryに流す」みたいなタスクはCPU・メモリの利用量が安定することが多いので, コンテナで動かすアプリケーションで十分対応可能です.

が, 「複数のデータセットをかき集めてクラスタリングした後機械学習の予測とかやります」だと,

  • データセットをかき集める, クラスタリングするときに多くのメモリが必要
  • 機械学習の予測タスクってCPU頼り(もしくはここだけGPU使う)なのでは?
  • その他の小さいタスクは分散するなり遅延処理するなりしたらさほどComputing Resourcesいらなさそう

とかあったりします. Resourcesに変動がある場合は,

  • Airflowの「ECSOperator*5」「KubernetesPodOperator*6」など, 実行時にリソース指定ができる仕組みを使う
  • kubeflowなど, k8s前提の流動的・弾力的にリソースが使える仕組みを使う
  • Sparkとかの仕組みに乗れるものはAWS Glue/EMRやGCP Dataprocなどフルマネージドな分散処理サービスに乗っかる

などの工夫で切り抜ける必要があります(かつ, LuigiやEmbulkなどはこの手の仕組みがありません).

開発のしやすさ(含む環境構築)

開発のしやすさもかなり大事です.

まず, ETLフレームワークはどれも癖があります&人それぞれ好みも別れます.*7

迷ったらやりやすいやつを選ぶのも大事な視点です.

比較表にしてみると...

というわけで, 上記の視点で比較表を作ってみました.

なお, あくまでも個人の感想です.

Framework 実行プロセス数 CPU・メモリ変動 開発しやすさ(shinyorke個人の感想) 備考
Airflow 複数コンテナ必要・Cloud Composerなどを使ってk8sで運用が推奨 k8sクラスタ内で割り当てたり他のサービス使ったりと優秀 DAGはPython書ければ誰でも書ける, 他は怪しい. オンプレ・セルフホスティングは地獄ですのでオススメしません, Cloud ComposerもしくはどうしてもせるふでやるならhelmのAirflowテンプレを使うのがベスト.*8
Luigi Docker Container一つで収まる 基本的に起動したコンテナ・マシンのリソースのみ Python完全に理解した...レベルならかろうじて行けそう 軽量で良い仕組みですが少し癖がある&後述のprefectが良い後継なので今からやるならprefectがオススメ?
prefect Docker Container一つでもk8sなどでもどっちでも 環境によって柔軟にできる(らしい) メチャクチャやりやすい, Luigiの代替として優秀 Airflowの代替として使えるっぽいが試した事無いです
kubeflow k8s k8s内でいい感じにやれる Pythonで書きやすそう(まだ書いたこと無い) 機械学習Workflowとしてすごく使えそう. データを移送する程度のETLとしては少々割高かも.

個人の感想が思いっきり入ってるので参考程度にしてもらえるとありがたいですが,

  • 比較的SLAが要求されるWorkflowはAirflow
  • 小さめのWorkflowはprefectやLuigi
  • 機械学習目的でkubeflow

ぐらいに雑に考えると良さそうです.

JX通信社におけるETLの使い分け

JX通信社ではデータ基盤チーム・CTO室で試行錯誤や本番運用しながらのトライアルの結果,

  • 全体的な大きめWorkflowはCloud Composer(Airflow)で管理
  • 1コンテナで収まるタスクはprefectとLuigiを使う, Luigiは順次prefectに書き換え中
  • DBのimport等小さいタスクはEmbulk
  • ML Opsが必要なWorkflowはkubeflowの導入を前提に色々とお試し中

という感じで進めています.

全体像

現状の全体像はこちらです.

f:id:shinyorke:20200726103741p:plain
個別のタスクでLuigi, Prefect, Embulk, 全体管理でAirflow使ってます

アプリケーションログを扱うか, RDBMS/KVSを扱うかで仕組みがちょっと変わっていますが原則として,

  • すべてAirflowのDAGから実行
  • 個別のタスク(小さいWorkflow)はAWS ECSもしくはCloud ComposerのPodとしてクライアントアプリ(Python製のコードもしくはEmbulk)を実行
  • 最後はBigQueryに投入, 中間データはS3/GCSに残す

という運用を行っています.

Airflow(Cloud Composer)

前述のとおり, Cloud Composerにすべてを任せています.

f:id:shinyorke:20200726114817p:plain
DAGで順番をコントロールしています

アプリケーション(AWS)側のタスクもAirflowのトリガーで実行(すべてECS Fargateのタスクとして実行)できるのでものすごく楽にできます.

GCPのタスクもCloud Composer内のnode pool上でPodを立てて実行しています.

また, DAGについてはビジネスロジックを原則書かない運用としているため, コードベースも比較的スッキリしています.

ちなみに, すべてのAirflowタスクがCloud Composerという訳ではなく, 一部セルフホスティングしているものもありますがこれは近々Cloud Composerに引っ越しする予定で進めています.

Luigi

アプリケーションログなど, ある程度のフィルター・クレンジングが必要(≒方言がある)モノはLuigi/prefect製のアプリケーションをECS Fargateのタスクとして実行し, ログを出力する仕組みとしています.

f:id:shinyorke:20200726115413p:plain
アプリケーションの方言が入るログは独自のETLでやってます

これは「Dockerのコンテナ1個で処理が終わる」レベルなので, Luigiみたいな小さいFrameworkが良さそうということでLuigiで開発しました.

luigi.readthedocs.io

Luigiは, Dockerコンテナ一つで動かすようなWorkflowや, 小規模な機械学習pipelineに用いることが多く, (前述の事例通り)私もよく使っていたので昨年までは愛用していました(が後述の理由によりprefectに移行中です).

prefect(Luigiの後釜として)

Luigiは実績があって枯れているETL Frameworkではありますが,

  • Workflowの定義・記法がやや独特. Luigiのクラス・インターフェースの定義に従って書かないといけない.
  • 「前のタスクが途中か終わってるか」的な管理を時前で実装する必要がある. 具体的にはタスクの開始・終了をログファイルやDBの状態で判断するような書き方になるため, 運用(特に再実行とか)の際にハマりどころとなりやすい.

という欠点があります.

どうしたものか...と思案していたところ, 最近注目を浴びているprefectを使うとこの辺がシュッとなると気がついたので最近はprefectでの開発に切り替えています.

docs.prefect.io

Airflowの開発者が, Airflowの辛い所をいい感じにするぜ!というノリで開発したETL Frameworkで, クラウドサービスも存在します.

感覚的にはAirflow的な使い方・Luigiの替わり両方行けそうな感じで, JX通信社では今の所後者(Luigiでやれそうな軽いWorkflow)で使っています.

prefectは, Luigiと異なり,

  • ETL/Workflowに必要な機能がいい感じにまとまっている. 並列実行・GUIなど面倒を見てくれる
  • Workflowに必要なタスク・実装をdecoratorで定義できる
  • Pythonの一般的なclass/methodの実装ができたら比較的シュッとできる

利点があり活用しています.

このエントリーだけでは説明がしきれないところもあるので, prefectの詳細については近日中に別記事で紹介いたします!*9

その他(Embulk, kubeflowほか)

RDBMSやDynamoDBなどを相手にするものは処理が決まってるためEmbulkを使っています.

また, 最近はML Opsまわりをもっと進めるため試験的にkubeflowも使ったりしています.

結び - 結局どれも癖がありますので長いおつきあいを

というわけでこのエントリーでは「ETLフレームワークの選び方・組み合わせ方」について色々と書きました.

が, これが最適解かと言われるとまだまだな気がしますし, この手の事例って意外と無いんですよね...

ご指摘や改善点などありましたらコメントや意見をいただけると嬉しいです :bow:

というのと, これだけは自信を持って言えるのですが,

ETLフレームワーク, 結局どれも癖がありますので長いおつきあいを前提にやってこうぜ!

どれも癖はありますが長所を活かす使い方をするとしっかりバリュー出ます.

色々と試し, 自学自習して改善しながらいい感じにやってくのがベストかなと思います.

最後までお読みいただきありがとうございました&次はprefect編で会いましょう.

*1:なお, 余談ですがこのネタ本当はPyCon JP 2020のCfPとして提出したモノの一部となります. 悔しいことに不採択になったので供養エントリーという意味合いも含んでおります(真顔)

*2:PyCon JP 2017の発表で, 日々発生する野球の試合データを収集・可視化する基盤のバックエンドとしてAirflowを使いました.

*3:私が当時在籍していたRettyでの事例で, アライアンス関係のバックエンドをLuigiでゼロから作った話です.

*4:Transformあたりは事実上なかったりする場合も無きにしもあらず(例えばExtractがSQLでデータを取ってくるとか)ですが, 最低でもExtractとLoadの2つはある認識でいます.

*5:特定のDocker ImageをAWS ECSのタスクとして実行する仕組みでFargateも使えるためかなり便利です. 後ほど触れますがJX通信社のデータ基盤でも活用しています.

*6:指定したk8sのクラスタ・node pool上のResourcesを使って処理を行う仕組みでこれもDocker imageを元に処理することになります.

*7:PythonのWeb Frameworkを思い出してみてください. Django, Flask, sanic, FastAPIどれも良いところ・癖がそれぞれあって好みが分かれることを. 他の言語も同じでしょう.

*8:いずれもk8s前提の運用となります.

*9:ホントはこの記事のメインコンテンツのつもりでしたが思ったより文量が増えたため別けることに

ReactのコードをHooksにリファクタリングしていく話

フロントエンジニアの渡辺(@pentla)です。

AI 緊急情報サービスの「FASTALERT」は、Reactをフロントエンドのスタックとして採用しており、3年ほどコードベースは大きく変えずに運用しています。その過程で、Flow → TypeScriptへのスタックの変更など、継続的にリファクタリングを進めています。

tech.jxpress.net

今回は、Classベースのコンポーネントを、Hooksを使って関数ベースのコンポーネントに書き直す話です。

今回の対象読者は、

  • Reactを書いたことがある方
  • Reactのパフォーマンスについて気になっている方
  • Hooksは知っているけれど、プロダクション環境に入れるか迷っている方

を対象にしています。

Hooksとは

v16.8から追加された機能です。今までは、Stateやライフサイクルメソッド(表示時に一度だけ呼び出したいときなど)の機能はClassを使って書く必要があったのですが、このHooksを使えば関数ベースのコンポーネントでもStateを扱えたり、「表示後に一度だけ呼び出したい」などの要件を満たすことができるようになりました。他にもHooksを使うことで利点があり、このあたりに書かれているので、実際に使おうと思っている方は一読の価値ありです。

ja.reactjs.org

それぞれのHooksの特徴

hooksはライフサイクルの考え方が少し特殊で、最初はやや違和感があるかもしれませんが、少しずつ慣れていくと、思い通りにコンポーネントの状態を管理することができます。 簡単なhooksの紹介をします。

useState

stateの書き方を変えるだけなので、最もわかりやすいです。

// before
constructor() {
  this.state = {
      isSample = true
  }
} 

// after
[isSample, setSample] = useState(false)

公式ドキュメント「useState」

useEffect

componentDidUpdateなど、特定のタイミングでのみ起動させたいときは、useEffectを使います。 気を付けたいのは、第二引数です。useEffectの中で使用している変数、関数についてはすべてuseEffectの第二引数の配列に入れます。この引数に値を入れることで、「この中の値に変化があった場合のみ、useEffectの中の処理を走らせる」ことができます。

// before
componentDidMount() {
  this.callAPI(this.props.id);
}

// after
useEffect(() => {
  callAPI(props.id)
}, [props.id, callAPI])

公式ドキュメント「ヒント: 副作用のスキップによるパフォーマンス改善」に詳しい処理の内容があります。

リファクタリングのモチベーション

Reactのライフサイクルメソッドに、今後いくつかの修正が入ります。具体的には、ComponentWillReceiveProps、ComponentWillMount、ComponentWillUpdateの3つの関数がdeprecatedになります。

この機能は、Reactのv17.0以降削除されることが決まっていて、その際はUNSAFE_というプレフィックスをつけることが求められます。 新しいライフサイクルメソッドを使って書き直すことも可能ですが、せっかくの機会なので、少しずつ関数ベースのコンポーネントに書き直しています。

Hooksに書き直したときの利点

コード量が減った

明らかに減ります。このようにstateを少し使った簡単なコンポーネントであれば、行数もかなりの数減らせることができます。21行から9行と、半分程度まで減っていますね!

Classベースのコンポーネント: 21行

export default class MemberList extends PureComponent<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { filter: { name: '' } }
    this.handleClickRow = this.handleClickRow.bind(this)
  }

 handleClickRow(rawId: number) {
    history.push(`/xxxx/${rawId}`)
  }

  render() {
    const { members } = this.props
    const {
      filter: { name }
    } = this.state
    // フィルタ機能
    const filteredMember = (members && members.filter(m => m.name.includes(name))) || []
    return <div>{...レイアウト}</div>
  }
}

関数ベースのコンポーネント: 10行

const OrganizationList: React.FC<Props> = ({ organizations }) => {
  const [filter, setFilter] = useState('')
  const handleClickRow = (rawId: number) => history.push(`/xxxx/${rawId}`)
  // フィルタ機能
  const filteredMember = useMemo(() => members && members.filter(o => o.name.includes(filter))) || [], [members, filter])
  return <div>{...レイアウト}</div>
}

export default MemberList

パフォーマンスの考え方がわかりやすくなった

Memo化を使うと、描画の際のパフォーマンスの調整が簡単になります。今までは、PureComponentsなどで描画回数を制限するなどしていましたが、useCallback, useMemoなどメモ化を使用することで変数ごとに計算回数の調整がしやすいです。

今までは、shouldComponentUpdate関数を使って、「このコンポーネントをいつ描画するか」を管理する必要がありました。例えば、下は、webページにタイトルを表示するReact.Compoentの例です。

class Title extends React.Component {
  render() {
   // すっごく重い処理
   const title = heavyFunction(props.title)
   return <div>{props.title}</div>
  }
}

class Parent extends React.Component {
  render() {
    return (
      <div>
        <Title title={this.props.title}/>
        {...その他いろいろなコンポーネント}
      </div>
    )
  }

Titleコンポーネントには、すっごく重い処理が書かれています。Titleコンポーネントは、Parentコンポーネントの内部にあります。 仮に、Parentコンポーネントが再度描画された場合、このTitleコンポーネントは変更がないにもかかわらず、再度描画処理が走ってしまいます。この場合、titleは変更がないにもかかわらず大きい処理が走ってしまい、パフォーマンスに影響が出るケースがあります。

そのようなケースに対応するために、

  • shouldComponentUpdate関数で、props.titleに変更がなければ描画しないようにする
  • pureComponentを使う

などの対策を取る必要がありました。

Memo化を使うと、このようになります。

const Title = props => {
  // すっごく重い処理は、props.titleが変更されない限り走らない
  const title = useMemo(() => heavyFunction(props.title), [props.title])
  return <div>{props.title}</div>
}

useMemoを使うことで、props.titleに変更がない限り、再度重い処理が走ることはありません。useMemoの使い方については、下にも説明を記載しています。

書き直していく上でのtips

ここからは、FASTALERTを実際にリファクタリングした時の話です。

小さめのコンポーネントから取り組む

特に最初はuseEffect、useCallbackなどの特徴を掴んでいくために、小さめのコードをFunctional Componentに書き直していくことをお勧めします。小さいコンポーネントであればその分修正も少なく、挙動の変更も確認しやすいです。

無理に書き直さない

特にテストがないコンポーネントの場合や、ライフサイクルメソッドに複雑な処理を書いている場合、大きな変更を入れるとどうしても変更が読めない箇所というのも出てしまいます。目的は将来のアップデートに追従するためで、必ず全てのコンポーネントをhooksを使って書き直す必要はないので、代わりのメソッドで置き換えるか、一旦後回しにする選択肢をとっています。

まとめ

今回のリファクタリングの途中で、

  • 書き直している途中に明らかにパフォーマンスが悪い箇所がある
  • APIの呼び出し箇所のコードがわかりづらく、簡潔でない

箇所に気づけたりして、問題になる前にコードの見直しができたのも良かったです。コードの行数も目に見えて減るので、効果がわかりやすいです。

さいごに - フロントエンドをいい感じにするフレンズまってます!

JX通信社では、FASTALERTやNewsDigestといったプロダクトの改善, 特にフロントエンドまわりを頑張っています。

上記以外にも現在進行系で行っている取り組みがあります、仲間を募集しているので興味があるかた気軽にお話に来てください!

www.wantedly.com

FastAPI で独自に定義した API エラーも仕様書に自動反映したかった話

こんにちは、サーバーサイドエンジニアの @kimihiro_n です。 最近は FastAPI という Python の Web フレームワークが社内で密かなブームとなっています。 今回はその FastAPI を使ったエラー定義まわりの話をしたいと思います。

FastAPI とは

FastAPI の概要については先日ちょうど社内勉強会用に資料を作ったのでこちらを見てもらえるのが早いです。 ざっくり言えばシンプルなインターフェースとドキュメント(OpenAPI)の自動生成が強力なフレームワークになります。

OpenAPI でドキュメント管理

今回注目したいのはドキュメントの自動生成のほうです。 開発チームでもドキュメントとAPIの実際の仕様が一致しない問題が時々発生していて、どうドキュメントを管理していくかが課題となっています。 KPTで振り返った結果、「人がドキュメントを書くからメンテが大変なのであって、ソースから仕様書が動的に生成されれば問題は解消するのではないか」という希望から FastAPI へと焦点があたりました。

このフレームワークでは入力(リクエスト)と出力(レスポンス)の形式を Python のモデルとして記述してあげることで、自動で OpenAPI 形式のドキュメントを出力してくれます。 OpenAPI はツールが充実しており、APIを試験的に叩くための Web コンソールを利用できたり、様々な言語から API へアクセスするときのソースを自動生成をしたりできます。

ちょうど小さめの開発プロジェクトが立ち上がったので、検証かねて FastAPI で作ってドキュメントが実用に耐えうるのかやってみました。

ちなみに、FastAPI は WSGI ではなく ASGI のフレームワークなので AWS のサーバレス構成で動かせるかは懸念だったのですが、ちょうどSREチームのたっちさんが AWS Lambda で動かすサンプルを検証してくれており、スムーズにプロジェクトに投入できました。

エラーレスポンス問題

実際 API を作って、フロントの開発メンバーに生成された OpenAPI を仕様書として渡してみたところ、正常系に関してはうまく機能することができました。 しかし どんなエラーをハンドリングしなくてはいけないのか という点に関しては生成された OpenAPI では判別できず、別途仕様を共有する必要がありました。

デフォルトだと正常系の 200 のドキュメントと、リクエストが指定したモデルと合致しているかの 422 エラーしか OpenAPI に記載されていませんでした。 認証が不正なのか、渡してるパラメータがおかしいのか、それとも権限・状態がおかしいのかという情報はクライアント側の挙動を適切にするためにも不可欠なものです。 5xx 系のサーバー内部のエラーは一旦置いておくとしても、4xx 系のエラーについてはドキュメントとして明記しておきたいです。

独自エラーを OpenAPI に記載する

FastAPI のドキュメントを調べてみたところ、Python の Exception をそのままレスポンスとして登録する手段は見つからず、responses という引数に OpenAPI の定義を付加情報として加えてあげる必要がありました。

# https://fastapi.tiangolo.com/advanced/additional-responses/ より
@app.get("/items/{item_id}", response_model=Item, responses={404: {"model": Message}})
    async def read_item(item_id: str):
    ...

responses は辞書になっていて、それぞれのステータスコードをキーに、レスポンスとなるモデルを記述(もしくは OpenAPI の 構造をそのまま記述)することで、生成されるドキュメントにエラー情報を付与することができます。

ただ実際のユースケースとして、

  • エラーレスポンスのデータ構造はほぼ共通で変わることがない
  • エラーで重要なのは構造よりエラーの中身
  • HTTP ステータスコードとしては同じだがエラーの理由が異なるケースがある

といったことがあげられます。

OpenAPI はインターフェースを記述するためのドキュメントなので、エラー内容まで関与しないというのは正しいのですが、そうするとエラー管理のための別のドキュメントを用意しなくてはなりません。 当初の目的としては API ドキュメントを1つにまとめて楽に管理することなので、なんとかこの OpenAPI 上でエラーの種別まで列挙するようにしたいです。

実際にやってみた

やっと本題になります。

上記のエラーの種別の列挙までを FastAPI で実現できないかをやってみました。

f:id:nsmr_jx:20200615134744p:plain

実現したい API ドキュメントとしては上記のようなイメージになります。

応答するステータスコードが列挙され、200以外のときはそのレスポンスの内容も網羅されているのが理想的です。 こうすることで API を叩く側がどんなエラーをハンドリングしなくてはならないのかが明確になり、連携を円滑に進めることが出来ます。 (HTTPステータスコードとは別にエラーコードを定義するのもよさそうです。)

併せて、Python 側のコードも複雑になりすぎないということを目標とします。 Open API の定義をそのままコードに埋め込めてしまうので、愚直に返しうるエラーを文字列で手動編集する、みたいなことは避けたいです。 認証のエラー文言が変わる度、全部のビューを編集するのはつらい…。

エラーを定義

まずは Python で API 独自のエラーを定義していきます。

# https://github.com/pistatium/fastapi_error_sample/blob/master/api/errors.py

class ApiError(Exception):
    """ エラーの基底となるクラス """
    status_code: int = 400
    detail: str = 'API error'  # エラー概要


class DontSetDummyParameter(ApiError):
    status_code = 403
    detail = 'ここに値をセットしないでください'


class InvalidFizzBuzzInput(ApiError):
    status_code = 400
    detail = 'input の値が不正です'


class WrongFizzBuzzAnswer(ApiError):
    status_code = 400
    detail = '不正解です'

ApiError という共通のエラーを定義してあげて、これを継承して具体的なエラーを列挙していきます。 同じエラーは同じ HTTP ステータスコードを吐くので、ここで一緒に定義してしまいます。

View でエラーを使う

# https://github.com/pistatium/fastapi_error_sample/blob/master/api/main.py

@app.post("/check_fizzbuzz", response_model=FizzBuzzRequest)
def check_fizzbuzz(req: FizzBuzzRequest):
    if req.dummy:  
        raise DontSetDummyParameter()  # # このパラメータを設定していたら強制的にエラーを発生。※実際は validator でやると綺麗
    try:
        expected = fizzbuzz(req.input)
    except ValueError:
        raise InvalidFizzBuzzInput()  # input の入力が不正であればエラー。※実際は validator でやると綺麗
    if expected != req.answer:
        raise WrongFizzBuzzAnswer()  # input と回答がマッチしなければエラー
    return FizzBuzzResponse(message='Good!')

例外を定義したら、FastAPI の View の中でその例外を利用するようにします。 ApiError を継承したエラーは HTTP のステータスコードをもってるので、View 関数の中でのみ利用するようにするとエラーの管理が簡単です。

エラーハンドラを登録する

@app.exception_handler(ApiError)
async def api_error_handler(request, err: ApiError):
    raise HTTPException(status_code=err.status_code, detail=f'{err.detail}')

独自のエラーを勝手に定義して raise するだけだと FastAPI がエラーとして認識できないので、500 エラーになってしまいます。 なので、FastAPI が ApiError を扱えるようエラーハンドラを追加します。 ここでは FastAPI の HTTPException にあわせて再度 raise しています。 こうすることで FastAPI 内部のエラーとも構造を共通化できるため、インターフェースとしてすっきりします。 もちろん独自の構造を定義して返してあげてもよいです。 HTTP ステータスコードとは別にユニークなエラーコードを振って返すようにすると、クライアントでもハンドリングしやすそうです。

ドキュメントにエラーを登録する

エラーハンドラを登録することでアプリケーションとしては問題なく動くのですが、OpenAPI の仕様書にはまだエラー情報が反映されてません。 自動でエラーを登録できると理想的なのですが、コードを静的に解析したりしない限りはどんな例外が発せられるかは把握できません。 なので半手動でエラーレスポンスを列挙することにします。

def error_response(error_types: List[Type[ApiError]]) -> dict:
    # error_types に列挙した ApiError を OpenAPI の書式で定義する
    d = {}
    for et in error_types:
        if not d.get(et.status_code):
            d[et.status_code] = {
                'description': f'"{et.detail}"',
                'content': {
                    'application/json': {
                        'example': {
                            'detail': et.detail
                        }
                    }
                }}
        else:
            # 同じステータスコードなら description へ追記
            d[et.status_code]['description'] += f'<br>"{et.detail}"'
    return d

このような ApiError を OpenAPI の書式に変換するユーティリティ関数を用意してあげます。 同じ HTTP ステータスコードのものが複数あると上書きされてしまうため、プログラム的に重複を確認し、ぶつかっていれば detail への追記を行うだけにしています。

そして先ほどの View 関数のルーティング部分を

@app.post("/check_fizzbuzz", response_model=FizzBuzzRequest,
          responses=error_response([DontSetDummyParameter, InvalidFizzBuzzInput, WrongFizzBuzzAnswer]))  # 追加
def check_fizzbuzz(req: FizzBuzzRequest):
   ...

のように変更します。 View 内で raise している ApiError をあつめて、error_response ユーティリティ関数へ渡しているだけです。 View の中で扱うエラーが増減したときは忘れずに反映する必要はありますが、View 以外で ApiError を発生させないみたいなルールとともに実装すれば管理しやすいかと思われます。

これにより、OpenAPI へのエラー定義を統一して扱えるようにできました。

OpenAPI で表示

FastAPI を動かしてみると上記のような OpenAPI の定義を生成することが出来ます。 開発サーバーの /docs にアクセスして UI ごと表示してもいいですし、 Python から定義を JSON として出力し外部のツールから利用することも可能です。

Swagger の Editor を利用するとこのような Web コンソールを出力できます。

GitLab だとデフォルトで OpenAPI に対応しているので、openapi.json をレポジトリに入れておくだけで勝手に UI 付きで表示してくれるので便利です。

おわりに

FastAPI を利用して API エラーも含めてドキュメント化することができました。 管理のコストを下げつついい感じの仕様書をつくれるので便利ですね。 コーディングする上でもはまりどころが少なく、FastAPI もっと活用していきたいと思いました。

説明に利用したコードのサンプルはこちら。

Flow/PostCSS の大規模プロジェクトを TypeScript/emotion に移行して数万行のプルリクを投げた話

JX通信社CDOの小笠原(@yamitzky)です。

AI 緊急情報サービスの「FASTALERT」は、報道機関や公共機関に導入いただいている(お堅めな) BtoB SaaS でありながら、 事業開始当初から React を使った Single Page Application(SPA) として作っています。 2017年には、より信頼性のあるフロントエンドを提供するため、 Facebook の Flow を導入しました。しかし、昨今の TypeScript の盛り上がりや、社内の他プロダクトで TypeScript を使っていることなどを受けて、フロントエンドのアーキテクチャを大幅に見直しました。

今回取り組んだ大きな変更は、

  • Flow から TypeScript への移行 (型チェックの移行)
  • TypeScript 化に合わせた、 babel-plugin-proposal-*** の廃止 (文法の移行)
  • PostCSS から [emotion] への移行 (CSS の移行)

などです。

今回のブログでは大規模プロジェクトにおいて、Flow から TypeScript、PostCSS から emotion へ移行した際の勘所や、知見などをご紹介します。Flow を使ったことないけど、JavaScript から移行したい方にも役立つかと思います。

Flow と TypeScript

Flow は Facebook が中心に作っている JavaScript のための静的型チェッカーで、TypeScript は Microsoft が中心に作っている JavaScript の型付きな上位互換言語(superset)です。 Flow は JS への型宣言拡張、TypeScript はプログラミング言語と、微妙な立ち位置は違いますが、共通するモチベーションとして 「静的型付き言語でない JavaScript に型安全を持ち込む」 というものがあるかと思います。

また、両者の文法も似ている部分があり、下記は TypeScript でも Flow でも同じような挙動をするコードです。

// @flow
function square(n: number): number {
  return n * n
}
square("2") // Error!

2017年に技術選定した当時は TypeScript の方が盛り上がるとは思っていませんでした*1。2020年現在、TypeScript の方が GitHub 上のスター数やサードパーティーの型宣言も多く、React との相性や DX(開発者体験) も良いと感じています。

PostCSS と emotion

React などの SPA でデザインを実装していく際には、グローバルな CSS ではなくモジュール化された(コンポーネントに閉じた)CSSを使うことが望ましいです。FASTALERT ではそのために、PostCSS を利用していました。

PostCSS を使うことによって、

  • モジュール化された、グローバルを汚染しない CSS の実現
  • 自動的な vendor prefix
  • ネストした CSS のような新しい構文
  • 変数を活用した DRY な CSS 定義

などが実現できます。一方で、emotion でも同様のことは実現できます。emotion は CSS in JS と呼ばれるような派閥の一つで、JavaScript(TypeScript) 内に CSS を書くことができます。

そのため、「CSS の構文で書くことができる」という点で PostCSS と emotion は共通してるものの、定義方法は大きく異なります。

PostCSS版

/* index.css */
.square {
  width: 100px;
  height: 100px;
}
// index.tsx
import styles from './index.css'

const Component = () => <div className={styles.square}>四角形</div>

emotion版

// index.tsx
const Square = styled.div`
  width: 100px;
  height: 100px;
`
const Component = () => <Square>四角形</Square>

PostCSS では CSS に書く、emotion では JavaScript (TypeScript) 内に直接書いています。

emotion の良いところは、なんと言っても React x TypeScript の流儀で書くことができる ことです! CSS プロパティも型定義されていたりと、型の恩恵 が受けられます。同じ理由で、Styled System を使ってのデザインシステムの構築もしやすいです*2

移行方法

mizchi 氏の「非破壊 TypeScript」 を参考にしつつ、次のようなステップで行いました。ピックアップしてご紹介します。

  1. TypeScript 向けにライブラリのインストールや設定変更
  2. .js の拡張子を .ts(x) にして、 // @flow のコメントをなくす
  3. babel を走らせると怒られるので、 TypeScript 文法の誤りを地道に直す
  4. tsc --noEmit するとやっぱり怒られるので、TypeScript の型エラーが発生したところを地道に解決する
  5. 無理だったら諦めて as any.js
  6. .css を .tsx 内に ひたすらコピペ
  7. eslint で自動フォーマットを走らせて微調整

これらの作業に特に面白いツールなどは使っておらず、基本的には力技と、VSCode の正規表現による置換、そして TypeScript に怒られドリブンで進めました。気合があればできる と思います。

TypeScript 向けライブラリの設定

次のような変更を行いました(かなり省略しています)

package.json

+    "@emotion/core": "^10.0.27",
+    "@emotion/styled": "^10.0.27",
....
-    "@babel/plugin-proposal-class-properties": "7.1.0",
-    "@babel/plugin-proposal-decorators": "^7.0.0",
-    (@babel/plugin-** が続くため略)
-    "@babel/preset-flow": "^7.0.0",
+    "@babel/preset-typescript": "^7.8.3",
+    "@typescript-eslint/eslint-plugin": "^2.13.0",
+    "@typescript-eslint/parser": "^2.13.0",
+    "babel-plugin-emotion": "^10.0.27",
+    "emotion": "^10.0.27",
-    "flow-bin": "^0.50.0",
+    "jest-emotion": "^10.0.27",
-    "postcss-cssnext": "^2.10.0",
-    "postcss-extend": "^1.0.5",
-     (postcss-* が続くため略)
+    "typescript": "^3.7.4",

.babelrc

-    "@babel/preset-flow"
+     "@babel/preset-typescript",
...
-    "@babel/plugin-transform-flow-strip-types",
-    "@babel/plugin-proposal-function-bind",
-     (@babel/plugin-*** が続くため略)
+    [ "emotion", { "labelFormat": "[filename]--[local]" } ]

.webpack.config.js

-      extensions: ['.webpack.js', '.web.js', '.js', '.jsx', '.json'],
+      extensions: ['.webpack.js', '.web.js', '.js', '.jsx', '.json', '.ts', '.tsx'],
...
-          test: /\.jsx?$/,
+          test: /\.[jt]sx?$/,
...
-          { loader: 'postcss-loader' }

特筆すべきは、Webpack 関連の設定を大きく変えずに済んでいる ことです。

TypeScript 化の方法は2つあり、

  • tsc コマンドなどを使い、TypeScript のツールセットで実現する
  • @babel/preset-typescript を使う

今回は後者にしました。すでに babel を活用している場合は、 @babel/preset-typescript を使うと簡単でおすすめです*3

無理だったら諦めて as any.js

「非破壊TypeScript」にもある通りロジックを変更しないのがとても大事です。ロジックを変更すると、なにかあったときの切り分けが難しくなります。ロジックを変更するぐらいだったら as any でキャストしたり、 .js のままで放置したりしました。

初めて TypeScript 化する場合は、積極的に諦めましょう! Done is better than perfect です。

たとえば、次のようなものにすら as any を使いました。*4

document.querySelector('.some-class-name').setAttribute('hidden', true as any)

.css を .tsx に移行

emotion では、次の2つの書き方が使えます。前者は css として書くことができる反面*5、後者では px を省略したり、パラメータをすっきりした形で使えます。

const Wrapper1 = styled.div<{ height: number }>`
  text-align: center;
  width: 240px;
  height: ${({ height }) => `${height}px`};
  & + & {
    margin-top: 4px;
  }
`
const Wrapper2 = styled.div<{ height: number }>({
  textAlign: 'center',
  width: 240,
  '& + &': {
    marginTop: 4,
  }
}, ({ height }) => ({
  height
}))

今回は「PostCSSから移行する」ということを念頭において、前者の CSS チックな書き方を採用してひたすらコピペしました。個人的には両方とも使いますが、主に後者の object 方式を使うことが多いです。

かかった期間

だいたいの作業としては、年末年始にテレビを見ながら2,3日でかきあげました*6。地道な作業が多いので、ゆっくりお酒を飲んだりバラエティ番組でも見ながらやりきるのがおすすめです。

移行してよかったところ

  • TypeScript 化によって、 Flow では気づけなかった型のミスが出てきた
  • PostCSS では気づけなかった、 使われてない CSS を削除できた
  • 社内メンバーがコントリビュートしやすくなった

1つめに関しては、React の 型指定の誤りや、型の指定が緩すぎるものなどが出てきました。Flow の使い方が悪かった可能性もありますが、TypeScript の方が型定義は厳しめな印象です。例えば、 Set の型パラメータの指定がなくて怒られました。無理なものは諦めた一方、地道な型解決でかなり型安全にできたと思います。

2つめに関しては、typescript-eslint の未使用変数チェックを通るため、使われてない CSS をあぶり出すことができます。仮にうっかり削除しても TypeScript のコンパイルエラーで気付けるので、削除の心理的障壁がありません。

3つめに関しては、TypeScript の方がエコシステムが充実していたり安心感があったりするのか(主観です)、プロジェクトのコアメンバーでなくてもコントリビュートしやすくなったように感じます。実際、FASTALERT 新型コロナ機能のプロジェクトでは、SRE のエンジニアやインターン生のコントリビューションもありました。

speakerdeck.com

まとめ

今回は、信頼性の求められる BtoB SaaS で、Flow を TypeScript に、PostCSS を emotion に移行したときの話でした。差分は数万行に渡ります*7。今回得たベストプラクティスな知見としては、

  • 非破壊 TypeScript を参考にする
  • TypeScript に怒られながら進める
  • ロジックは絶対に変えない、積極的に諦める
  • なにかのついでにやると、地道な作業が多くても問題ない

以上です。後学のため、もしツールなどでスマートに移行する方法があれば教えて下さい!

宣伝

今回ご紹介したとおり、JX 通信社のフロントエンドプロジェクトは、 TypeScript、React、emotion が活用されていたりとモダンな開発環境です。フロントエンド(や Pythonも!) を一緒に書きたいインターン生を積極募集しています。まだ新型コロナも収束していませんので、エリア不問&リモートOKです!

*1:Flow も React も同じ Facebook 由来のため

*2:新型コロナダッシュボード爆速リリースの舞台裏 〜小さく始めて大胆に変えるフロントエンドプロジェクト〜 - JX通信社エンジニアブログ で紹介したような共通ライブラリ化が容易というメリットなどもあります

*3:ただしトランスパイル時の型チェックはされないので、 tsc --noEmit を走らせて型チェックをしてください

*4:ちゃんとやれば any を使わず書き換えられますし、なんならもっと React っぽく移行できそうですが、動いていたものを最優先

*5:プラグインによる色付けもしやすいと思います

*6:他作業もあり、マージされるまでにはこれ以上かかっています

*7:正直に言うと、 eslint の自動フォーマットもあるので盛ってます

新型コロナダッシュボード爆速リリースの舞台裏 〜小さく始めて大胆に変えるフロントエンドプロジェクト〜

JX通信社CDOの小笠原(@yamitzky)です。

JX通信社は「今起きていることを明らかにする報道機関」というミッションの元に、新型コロナリアルタイムダッシュボードを 2月16日 から提供し続けています。今回は、「新型コロナプロジェクト」の発足から現在に至るまでの、プロジェクトの進化についてご紹介します。

f:id:yamitzky:20200529014632p:plain

プロジェクト発足

そもそものプロジェクトの発足としては、2月14日の下記のツイートが発端でした。およそ 2 日でリリースしたことになります。

当時は、東京都のような自治体公式の特設サイトや、国内全体の動向をまとめたサイトはほぼありませんでした。国内の公共機関・報道機関のなかで、かなり速くリリースできた部類に該当するかと思います。

フェーズ1: Vue.js でのプロトタイピング

現在は React で作られているプロジェクトですが、当初のプロトタイピングフェーズでは Vue.js を使っていました。

Vue.js は Progressive(漸進的) なウェブフロントフレームワークです。最もシンプルなのは、次のような単体の HTML として配信する方法です。この形式ではトランスパイル(Webpack によるビルド等)の必要もなく、HTML 単体を配信するだけでも SPA になり、非常に手早くプロジェクトを開始できます。

// index.html
<html>
  <body>
    <div id="app">{{ message }}</div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: () => ({ message: "hi" })
      })
    </script>
  </body>
</html>

今回のプロジェクトでは「なるべく早くリリースする」という目的があり、ツール周りでつまずきたくなかったので、まずは Vue.js で雑にプロトタイピングしました。

フェーズ2: React × TypeScript × Emotion × Parcel への移行]

f:id:yamitzky:20200529030051p:plain

プロトタイプとして実現したものの、NewsDigest のウェブメディアで配信したいという課題がありました。NewsDigest の CMS は、Django (Python) で管理しており、必ずしも SPA との相性が良いとは言えません。

そこで次のような形で、 CMS にたった 2 行のタグを埋め込むだけで新型コロナのダッシュボードを配信できるようにしてみました*1

f:id:yamitzky:20200529020642p:plain

技術的な必要条件としては、

  • 1 つの script タグに JavaScript の依存関係を全て詰め込む
  • 1 つの script タグさえ埋め込めば、デザイン(stylesheet) も再現できる

です。これを最短で実現するため、バンドラー(Webpackの代替)として Parcel を利用しました。Parcel はほぼ設定不要で使えるバンドラーなので、今回のような急ぎのプロジェクトにピッタリです。また、CSS ファイルに依存せずデザインを指定するため、Emotion も使っています。Emotion は React と相性の良い TypeScript ベースのデザイン用ライブラリで、JS 内にデザイン指定を埋め込めます。

// index.tsx
const Wrapper = styled.div({
  padding: 8,
  color: 'red',
  display: 'flex',   // vendor prefix も自動付与
})
export const Component = () => <Wrapper>some content</Wrapper>

つまり、プロトタイピングから本番配信までの間に、Vue.js から React・TypeScript にまるっと置き換えたことになります*2

フェーズ3: gitでのデータ管理から、サーバーレスなデータ管理へ

新型コロナの感染者数字は、リアルタイムに更新されます。実は、プロジェクト初期ではデータベースを用意しておらず、TypeScript のソースコード内に直書きする形でデータ管理をしていました。数値更新するたびに GitLab CI が動いて、JS ファイル自体をデプロイしていたのです・・・!

f:id:yamitzky:20200529130138p:plain

// 実際の data.ts
export const data = {
  modified: '2020.02.19 11:30',
  japanStats: {
    infected: 616,
    infectedChange: 96,
    dead: 1,
    deadChange: 0,
    // クルーズ船(ダイヤモンド・プリンセス号)
    cruiseInfected: 542,
    cruiseInfectedChange: 0
  },
   ...
}

さすがに、機械化しづらい、型安全でなく事故が起こりやすい、git でデータ更新をするのは属人性が高い、Python 製の FASTALERT API としての提供が難しい... という事情もあり、Google Spreadsheet をデータベースとして利用し、JSON から使えるようにしました。また、データ更新の仕組みは GitLab CI ではなく、AWS Lambda でサーバーレスな形で動かすようにしています。

f:id:yamitzky:20200529130900p:plain

フェーズ4: Next.js での SSR への移行

<script> タグでの配信は、SEO 的に弱い可能性があるのではないか、という懸念が生まれました。そこで当初の Parcel から、Next.js へ移行しました。Next.js はReact のフレームワークで、サーバーサイドレンダリングの機能などが組み込まれています。

この Next.js のサーバーは、Amazon ECS(Fargate) 上にデプロイしています。

フェーズ5: ウィジェットとしての外部提供と yarn monorepo 化

JX 通信社が提供している新型コロナダッシュボードは、メドピア社LINE社にウィジェットとしても提供しています。また、NewsDigest だけでなく FASTALERT 内でも配信しています。NewsDigest での提供、ウィジェットとしての提供、FASTALERT 内での提供、、、これらを 1 プロジェクトでやるのは現実的ではなかったため、次のような monorepo 構成を行っています*3

f:id:yamitzky:20200529164607p:plain

@corona/components ・・・ 各種グラフを提供するライブラリ。FASTALERT からも npm install している
@corona/server ・・・ @corona/components に依存する、next.js のプロジェクト。newsdigest.jp 用
@corona/widget ・・・ corona/components に依存する、webpack のプロジェクト。ウィジェット配信用

この monorepo 構成は、yarn の workspace 機能を使っています。

また、@corona/server と @corona/widget では、ビルドツールなども異なっています。これは、プロジェクトの目的や、求められる安定性*4などに応じて、意図的に使い分けたものです。

うまく monorepo 構成にすれば、コードを使いまわしつつ、最適な技術選定ができるようになります。

フェーズ6: グラフライブラリの移行

プロジェクト当初は、chart.js と d3 などを使っていましたが、 vx という React 向けのグラフライブラリに移行しました。

Chart.js は canvas ベースのグラフライブラリです。しかし、 React のような宣言的な UI の思想との相性の悪さや、サーバーサイドレンダリングできない、柔軟にカスタマイズできないなどの課題がありました。

d3.js もデータ可視化に使っていましたが、厳密には「ドキュメントをデータに基づいて操作・構築するためのライブラリ」です。雑に言えば「DOMを操作するためのライブラリ」なので、 React(React-DOM)のような DOM 操作のライブラリと、役割的にかぶっています。

そこで、 React ベースの低レイヤーなデータ可視化ライブラリである vx に移行し、React だけで SVG での可視化をしています。

  • React 的な、宣言的データ可視化の実現
  • TypeScript の型
  • 柔軟なデータ可視化の実現

などができるようになりました。一方で、パフォーマンスチューニングや、コード量の増加などは起きています。

f:id:yamitzky:20200529115825p:plain
凡例や横軸の位置など、かなり微調整しています

まとめ

新型コロナウイルスに関しては、社会的な需要や、刻々と変わる状況などを踏まえ、かなりスピード重視でプロジェクトが発足しました。当初は git でデータ管理していたほど、一般的なアプリケーション構成のセオリーからは外れた作り方です。

一方で、新型コロナダッシュボード提供開始から3ヶ月経ち、ビジネスの状況に応じて大規模なアーキテクチャ変更(式年遷宮)を行っており*5「ビジネスとスピードと品質の両立」も実現できたと思います。これらの両立のポイントは「ビジネス要件に合わせて技術を使いこなし、いかに漸進的に進めるか」です。

f:id:yamitzky:20200529132312p:plain

今回のプロジェクトでは、かなりインターン生に協力いただいています。本当にありがとうございます! 引き続き、フロントエンドのエンジニアインターンを募集中です(まだ新型コロナは収束していないのでリモート中心です)。ぜひ一緒に、インパクトのある開発をしましょう!

*1:この JS 自体は、Amazon S3 と CloudFront で配信しています

*2:同じようなことを実現する方法は他にもあります。この技術選定は、慣れの部分が大きいです

*3:データ更新用サーバーレスパッケージなども含めると、10 個ほどパッケージが含まれています

*4:ウィジェットは絶対にサーバーを落としたくないため、SSR は行っていません。また、顧客ごとにカスタマイズする関係で SSG にもしていません

*5:プロジェクト発足から 3 ヶ月ぐらいしか経っていないのに、1000行以上差分のあるプルリクが複数あります。同僚から「走りながら車輪を全交換した話」というタイトル案が出るくらいです