# Wantedly Engineering Handbook

新しくウォンテッドリーの開発チームに参加する人向けのドキュメント集です。社内のエンジニアが知るべき情報のうち外部にも公開できる情報を体系的にまとめたものです。

入社前後のフルタイムの社員が一番の想定読者です。ハンドブックの内容はインターンや採用選考を受けている人にも役に立つことを期待しています。また、PDF 形式の電子書籍およびオンラインドキュメントとして広く一般公開しています。1 年に 1 度、物理書籍としても印刷し社内外に配布します。

## 入手

* [🌏 公開版 PDF (技術書典)](https://techbookfest.org/product/4831016940208128?productVariantID=4856256852393984)
* [🔒 社内版 PDF (internal)](https://github.com/wantedly/dev/releases)
* [🌐 Web 版](https://docs.wantedly.dev)
* [📚 過去の TechBook](https://drive.google.com/drive/folders/1sY2eF93rTaNkQeqIgtnNZntXpUueQ5DZ)

## 関連資料

* [📊 Wantedly の開発チームをよく知ってもらうための 2W1H](https://speakerdeck.com/wantedly/for-engineers)
* [🎤 登壇資料一覧](https://wantedlyinc.com/ja/presentations)
* [🎧 Podcast](https://anchor.fm/wantedly-dev)
* [🍿 YouTube](https://www.youtube.com/channel/UCj1HXgKShVMrIn9EFetN7jw)
* [🛋 Blog](https://www.wantedly.com/stories/s/wantedly_engineers)
* [🇬🇧 Blog (English)](https://medium.com/wantedly-engineering)
* [🇬🇧 Blog (English - dev.to)](https://dev.to/wantedly)
* [💬 Twitter](https://twitter.com/wantedly_dev)
* [🏫 wantedly/learning-resources](https://github.com/wantedly/learning-resources)
* [📚 dev-docs (internal)](https://dev-docs.wantedly.com)
* [🔰 ウォンテッドリー入門 (internal)](https://dev-docs.wantedly.com/beginners/)
* [📚 Confluence (internal)](https://wantedly.atlassian.net/wiki/home)

## Dashboards

* [Looker (internal)](https://wantedly.jp.looker.com)
* [SLO/SLI (internal)](https://slo-dashboard.internal.wantedly.com/)
* [Argo CD (internal)](https://argo-cd.internal.wantedly.com/)
* [Argo Workflow (internal)](https://argo.wantedly.com/)
* [Loki (internal)](https://loki.wantedly.com/)
* [Datadog](https://app.datadoghq.com/dashboard/lists)

## 目次

* [まえがき](/preface)

### 第一部：開発チームへの案内

* [技術とアーキテクチャ](/introduction/technical-overview)
* プロダクト概要
* [開発チームの構造](/introduction/dev-org)
* [コミュニケーションの全体](/introduction/communication)
* ドキュメンテーション
* [カレンダー](/introduction/calendar)
* [障害対応の心構え](/introduction/incident)
* [効率的な社内知識の調べ方](/introduction/catching-up-on-in-house-knowledge)
* [外部発信](/introduction/outreach)

### 第二部：技術領域への案内

#### Apps

* [アプリを提供するプラットフォーム](/fields/apps/apps_platforms)
* [デザインシステム入門](/fields/apps/design-system-for-non-designers)
* [Web アプリのアーキテクチャ](/fields/apps/frontend-architecture)
* [プロダクトデザイナーと上手に協働するための心得](/fields/apps/collaboration-with-designers)
* [Web アプリのデザインシステムライブラリ](/fields/apps/design-system)
* [Web アプリ共通ライブラリ "React Shared Component" の紹介](/fields/apps/react-shared-components)
* [モバイルネイティブアプリについて](/fields/apps/mobile-native-app)

#### The System

* [protobuf スキーマと gRPC 通信](/fields/the-system/apis)
* [実践: gRPC in Ruby](/fields/the-system/grpc-in-ruby)
* [実践: gRPC in Go](/fields/the-system/grpc-in-go)
* [GraphQL Gateway - アプリ向けに API を公開する](/fields/the-system/graphql-gateway)
* [Wantedly Visit で BFF GraphQL サーバーを辞めた理由](/fields/the-system/history-of-graphql)
* 実践: GraphQL スキーマ設計(未執筆)
* [API での認可処理 (Authorization)](/fields/the-system/authorization)
* [マイクロサービス共通ライブラリ "servicex" の紹介](/fields/the-system/servicex)
* [CronJob](/fields/the-system/cronjob)
* 非同期メッセージング処理入門(未執筆)
* バッチ処理入門(未執筆)

#### Infrastructure

* [Infrastructure Squad](/fields/infrastructure/infrastructure-squad)
* [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction)
* [インフラ構成概要](/fields/infrastructure/infrastructure)
* [リリース・デプロイ戦略を支える技術](/fields/infrastructure/deploy-strategy-implement)
* [トラブルシューティング: 何を見たいときに何を使うか](/fields/infrastructure/troubleshooting)
  * [Honeybadger](/fields/infrastructure/troubleshooting/honeybadger)
  * [Datadog](/fields/infrastructure/troubleshooting/datadog)

#### Data

* [データ基盤入門](/fields/data/data-infra)
* [レコメンデーション](/fields/data/recommendation)
* [Looker 入門](/fields/data/looker)
* [推薦システムの開発に使っているツール](/fields/data/ds_tools)

#### 開発プロセス

* [Git の慣習](/fields/dev-process/git-convention)
* [Pull Request の書き方](/fields/dev-process/how-to-write-a-pull-request)
* [ポストモーテムの取り組み](/fields/dev-process/post-mortems)
* [負債返済日の取り組み](/fields/dev-process/fusai-hensai-bi)
* [プロダクトの課題発見及び解決](/fields/dev-process/discovering-and-solving-service-issues)
* [ソフトウェアデザインの基礎](/fields/dev-process/software-design)
* [コーディング規約](/fields/dev-process/coding-rule)
* [リリース・デプロイ戦略](/fields/dev-process/deploy-strategy-overview)
* [上長承認が必要な作業](/fields/dev-process/approval)
* [アーキテクチャディシジョンレコード(ADR)](/fields/dev-process/adr)
* [作業ログを残す意味](/fields/dev-process/operation-log)
* [多言語化対応(i18n)](/fields/dev-process/i18n)
* [メール開発](/fields/dev-process/mail)

#### 開発ツール

* [kube](/fields/dev-tools/kube)
* [Code Coverage](/fields/dev-tools/code-coverage)
* [Kubefork](/fields/dev-tools/fork)

### おわりに

* ロードマップ(未執筆)
* [Handbook の書き方](/postscript/guideline)
* [コントリビューター](/postscript/contributors)

### 付録

* [社内用語集](/appendix/jargon)
* 主要な GitHub リポジトリのリスト(未執筆)
* 今後の挑戦・未解決イシュー(未執筆)
* プロダクト開発組織のバリュー(未執筆)
* 採用についての考え方(未執筆)

## コントリビューションガイド

ハンドブックは、継続的にチーム全員で更新していきます。特に、新人のみなさんの積極的な貢献を望みます。自分自身が困ったことや、ドキュメント不足に気づいた情報を、未来の仲間のために書き残してください。 既存の更新案は [Wantedly Engineering Handbook](https://github.com/orgs/wantedly/projects/115) で管理されています。

### 情報の追加・更新や誤字脱字

修正の pull request を歓迎します。[@engineering-handbook-editors](https://github.com/orgs/wantedly/teams/engineering-handbook-editors)にレビューをアサインしてください。

### 新しい記事の執筆

目次の中でリンクが存在しない記事は未執筆です。少しでも情報があれば助けになります。短い文章や、GitHub issue の抜粋でも構わないので、関連しそうな情報を教えて下さい。記事を書いて pull request をだすのも、もちろん歓迎です。

また、現時点で目次に含まれていないトピックに関しても、新しい記事の提案を歓迎します。気軽に[issue](https://github.com/wantedly/dev/issues?q=is%3Aissue+is%3Aopen+label%3AHandbook)を作成してください。

新しい記事を作成し、書籍版に含められる内容になったら、[catalog.yml](https://github.com/wantedly/dev/blob/master/handbook/catalog.yml)に追加してください。

書籍版にまだ含めない場合は、記事の先頭に `<!-- toc-skip: book (理由) -->` を追加してください。Web 版にもまだ載せない場合は `web` も追加できます (例: `<!-- toc-skip: web, book (理由) -->`)。理由は必須です。

記事の文章フォーマットに関しては、[Handbook の書き方](/postscript/guideline)を参照してください。


# まえがき

ウォンテッドリーの開発は2011年にスタートし、その歴史は10年を超えました。プロダクトとビジネスの拡大にあわせて、それを生み出す組織は大規模に、システムは複雑化し、変化をし続けています。

さて、この本は新しく開発組織に参加するソフトウェアエンジニアを主な対象読者として作成されました。 開発組織で最高のパフォーマンスが発揮できるようになるまでには、組織構造、アーキテクチャ、要素技術、リリース戦略、コミュニケーションチャネルと多岐に渡る知識が必要となります。それらの知識は少なからず組織のコンテキストと結びついていて、独自に獲得することは難しい性質のものです。また、時を重ねるにつれて、言語化されてない知識や、属人的に口伝で一部のみに共有されている知識が増え、組織全体での共有が難しくなる傾向にあります。本書にはそのような知識の中でも特に初期に知るべき内容を集約しています。この本が開発面でオンボーディングの道標となり、いち早く良い仕事をできるようになる一助となることを狙っています。また、本書の作成プロセスを通じて、知識の言語化および非属人化を進めることも目指しています。

この本は、社内ドキュメント版と書籍版(物理本及び電子本)の2つの形態をもちます。いずれも内容は同じで、継続して更新される社内ドキュメント版を本として広く社外にも配布出来る形にしています。入社を検討してくださっている社外の方に読んでいただくことで、開発プロセスや技術へ興味を持っていただき、より具体的な働くイメージを持っていただくことも、この本の副次的な効果として期待しています。コーポレートミッションやバリューを伝えるためのもう1つの冊子、Wantedly Culture Book と対になる形で、本書 Wantedly Engineering Handbook を読んで頂けると幸いです。

また、この本はアップデートをし続けることを前提とした永遠のベータ版です。Wantedly のプロダクトとビジネスが時代とともに変化し続けるのと同様に、プロダクトを生み出すために必要となるプロセスや技術も今後も継続して変化し続けるでしょう。年に1回おおきく内容を見直してアップデートを行う予定です。今後の更新を楽しみにしてください。

この本に関する最新の情報は <https://wantedly.dev/> を参照してください。


# 技術とアーキテクチャ

![](/files/-MhWAO_m4UQWNegjfu0b)

この図は、Wantedly というサービスの向かっているソフトウェア構造を大きなレベルで抽出し（アーキテクチャ）、それぞれの部分でどのような技術を使っているのかを重ねたものです。この章では、この図を元にして Wantedly のアーキテクチャと技術について概観します。

## アーキテクチャ

Wantedly のサービスは、各々が独自のリリース・サイクルを持つ多数のソフトウェア・コンポーネントの組み合わせによって動いています。

例えば、iOS というプラットフォームにおいて提供している Wantedly アプリは、App Store に申請を行い、申請が通ってリリースされることでユーザーの目に見える改善・機能追加となります。同様に、Web というプラットフォームにおいても、React を使ったアプリが（多くの場合は iOS よりも短いリリース・サイクルで）改善されていきます。

このように多数のアプリが異なるプラットフォームで提供されているため、コンポーネントの数としては一定数存在します。

ほとんどのアプリは、「募集」や「プロフィール」などのデータを扱うため、バックエンドのシステムを必要とします。現在、多くの箇所ではバックエンドとは API を通じてやり取りしています。

バックエンドのシステムは、多数の個別にリリース可能なソフトウェア・コンポーネントからなっています。ただし、積極的にドメインを分けることを目的としているわけではなく、必要に応じて分化することができる進化的なアーキテクチャを採っています。コンポーネント数としては二桁オーダーで存在します。

アプリおよびシステムからは様々なデータが生まれます。これらのデータは、BigQuery というデータウェアハウスに一元的に集約していて、サービスの推薦機能や改善のための意思決定に使われています。実際のところ、このレイヤーは単なるデータの置き場所ではなく、我々の立てた Kubernetes クラスタ上のシステムではなかなか扱いづらい大規模なデータの分散処理基盤としても大いに活用しています。

## 技術

### アプリ

アプリを記述するプログラミング言語は、それぞれ TypeScript、Swift、Kotlin を採用しています。Wantedy のサービスが開始した時点では、各プラットフォーム上のプログラミング言語は JavaScript、Objective-C、Java でした。これらはどれも80年代-90年代に設計されたプログラミング言語であり、よく設計されてはいますが積み重なった課題も多く、アプリのリニューアルなどのタイミングで積極的なリライトを行なっています。

プログラミング言語以外では、Web における UI 構築のために2016年から React を導入しています。それまでは Ruby on Rails の Haml によるサーバーサイドレンダリングでしたが、優れた UI/UX を実現するためにクライアントサイドにおけるレンダリングに切り替えています。

このような抽象化技術の導入は、2017年に Wantedly Design System を定義してから、デザインシステムライブラリの構築などにも役立っています。Wantedly Design System は、異なるプラットフォームで提供する場合のデザインの重複をなくすと同時に、サービスを利用するユーザーには共通の UI/UX を提供することを意図しています。

また表層から離れると、モバイルアプリの iOS と Android におけるビジネスロジックの共通化を行う技術として、Kotlin Multiplatform (KMP) を導入しています。

### システム

ウォンテッドリーのサーバーは全て Docker コンテナの中で動作し、Kubernetes というコンテナオーケストレーションの基盤の上で動いています。これらはマイクロサービスの単位で独立してテスト・デプロイされます。

Docker や Kubernetes のような抽象化技術を導入しているため、マイクロサービスの実装に複数のプログラミング言語を使う選択肢が採れます。ウォンテッドリーで現在サーバーの実装に使われる主要な実装言語は Ruby, Go, Python です。マイナーなものとしては TypeScript、Rust があります。

Ruby は一番最初から使用していた言語で、サービスに必要な機能を一通り高速に実装し、改善していく用途で今も多くの箇所で使用しています。主要なドメインのデータは Rails のサーバーに置くことがほとんどです。

Go は開発のリードタイムよりも安定性やスケーラビリティが求められ、責務が明確なマイクロサービスの実装に使用しています。1プロセスが軽量であったり、並行処理の機構が充実しているのも、Kubernetes 上で動かすマイクロサービスの実装と相性が良いです。

Python は機械学習・推薦を行うマイクロサービスやバッチジョブで使用しています。例えば、Wantedly People の名刺を取り込む際には、「文字列のカテゴリーを当てる」「読み仮名を推定する」などのタスクをそれぞれマイクロサービスで処理しています。

こういった異なる技術で実装されたコンポーネントを上手く協調して動かすためには API がとても重要になります。そのために、Protocol Buffers という実装と紐づいたスキーマ記述言語を使用しています。

Protocol Buffers はシリアライゼーション・フォーマットでもあり、そのデータは任意の方法で送ることができます。ウォンテッドリーの場合、gRPC で同期的にやりとりを行うか、メッセージング・サービスである Google Cloud Pub/Sub で非同期的にやりとりを行うかの二つの手段を用意しています。マイクロサービス・アーキテクチャのような分散システムにおいては、全てを同期的にやりとりしてしまうと強い依存関係が生じてしまうため、非同期メッセージングが適した箇所では Pub/Sub も利用しています。

システム内部の通信は以上の手段を推奨していますが、アプリから呼び出す際には、GraphQL の gateway サーバーを叩きます。GraphQL は我々が提供したい各アプリ・プラットフォームにおいてエコシステムがあること、そして UI にどこまでの情報を表示するかによって API が返す情報量がしばしば変わるという、普遍的な要件に対して解決をしていることから採用しています。ただし、API にスキーマを与えることは Protocol Buffers で解決しているため、この gateway サーバーは Protocol Buffers から半自動生成した薄いものとなっています。

最後に、さまざまな技術に触れましたが、これら全てが同時に必要になることはないので、安心してください。ここでは、ウォンテッドリーで利用している技術とそのつながりのインデックスを作ってもらうことで、Wantedly Engineering Handbook を歩きやすくすることを意図しています。

#### これまでの変遷

![](/files/0BCpQek53AeLCFetZpvq)

#### 話を聞きに行きたい

* Slack: [#engineering](https://wantedly.slack.com/archives/CQK61054H)

#### もっと知りたい

* [\[Railsdm 2018\] Wantedlyにおけるプロダクト、技術、組織、7年間の進化 - YouTube](https://www.youtube.com/watch?v=VNhxyajRKBg)
* [Wantedly における Go 導入にまつわる技術背景 | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/193633)
* [Wantedly の技術とアーキテクチャの変遷](https://miro.com/app/board/uXjVPzWWaNA=/?share_link_id=524733341958) (internal)
* [継続的なサービス発展を支えるアーキテクチャと技術 - Speaker Deck](https://speakerdeck.com/altech/the-architecture-and-technology-for-continuous-services-growth)
* [主要なアーキテクチャ課題と解決指針](https://backstage.internal.wantedly.com/docs/default/component/dev/architecture_overview/) (internal)


# プロダクト概要(未執筆)


# 開発チームの構造

技術とプロダクトと組織は相互に関係しています。ここでは、どのような人の集まり=組織が、技術を使ってプロダクトを生み出しているのかの概要を説明します。

![](/files/ijYq1M9Mxb18y9ZrZXv1)

こちらの図は2026年時点での組織の構造をおおまかに描写したものです。

## Squad

まず、組織にはたくさんの **Squad** と呼ばれる2-5人程度のチームがあります。Squad は、共通のミッションを達成するための、自律した最小のチーム単位です。Squad には、そのミッションを達成するために必要な、さまざまな技術領域・職能の人が集まっています。ミッションは OKR: Objective and Key Result によってわかりやすくチャレンジングなものとして定義します。

特にプロダクト開発に関して Squad を活動の基本的な単位とするのは、プロダクトのミッション（目的）に対して共感して動ける文化を重視しているためです。職能を先に置いてしまうとこういった要素が薄くなりがちです。

## Chapter

もう一つ重要な単位に **Chapter** と呼ばれるものがあります。これは、Squad を横断して存在する、共通の専門領域に関心がある人たちの集まりです。専門領域に対して適切な戦略を考えたり、共通の課題に取り組んでいます。

例えば、システムを開発していて、自分の Squad では通信に JSON を使っているけれども、隣の Squad では XML を使っている、といった事があると Squad に入った時にうまく能力を発揮できませんし、開発した機能間の連携もできません。そのような機会があまりなければ良いですが、Wantedly は人と会社が有機的に価値を生み出す単一のビジネス・プラットフォームであるため、機能間の異動や連携は恒常的にあります。そのため、Chapter という仕組みが存在します。

Squad と Chapter の違いが一点あります。Squad については、素早く動きユーザーに価値を届けることを重視するために原則としてひとつに所属している状態が望ましいです。一方で Chapter についてはあくまで技術的なトピックの切り口であるため、そういった状態を理想としているわけではありません。必要であれば新しい領域にチャレンジするのも良いでしょう。

なお、技術領域と言ってもどこまでも細かく切ることは想定しておらず（e.g. "Ruby Chapter" という単位では Chapter にしない）、さまざまなことを考慮して一定の粒度で決めています。大まかには、1) 身につけるのに一定以上の時間が必要なコアスキルのセットを共有している、2) 同様のスキルセットを持つエンジニアが世の中や社内に一定数いる、といった条件は満たしている必要があるでしょう。

以上が、もっとも基本的な組織の構成要素です。他にも、補完的なものとして Guild というものもあります。

## 全体

最後に、より具体的な Squad と Chapter を含む全体について説明します。プロダクトに紐づく Squad は、大きな目的ごとに編成された **Tribe** と呼ばれる単位の傘下で動いています。また、ソフトウェア基盤の構築・改善、意思決定のための情報収集分析、組織開発などプロダクト開発をより推進するための基盤 Squad も存在します。例えば Kubernetes のようなシステム基盤は Infra Squad によって導入・運用をしています。プロダクト開発 Squad と基盤 Squad があることで、生産と生産性を両立することを目指しています。

#### もっと知りたい

* [SquadとOKR - 開発チームが無駄なく高い成果を出すために大事にしていること | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/134301)
* [Guild](https://backstage.internal.wantedly.com/docs/default/component/dev/guild/) (internal)
* [FY26 組織図](https://miro.com/app/board/uXjVJLPRnuY=/) (internal)


# コミュニケーションの全体

開発チーム内のコミュニケーションの仕方は、開発しているソフトウェアと同様に、設計した上で継続的にリファクタリングをしていっています。このページは、そういったコミュニケーションの最新の状態をまとめています。

## 基本的なコミュニケーションチャネル

基本的なコミュニケーションのチャネルとして三つ利用しています。

* ミーティング
* チャット
* リポジトリ

上から下へ、同期的なチャネルから非同期的なチャネルになっているため、用途によって使い分けています。

こういったグラデーションは、エンジニアが高い生産性を出すために必要です。同期コミュニケーションは仕事を進める上でもちろん重要ですが、用途によって使い分けることで、不要なインタラプトを減らして、コーディングなど集中して行うべき仕事も遂行しやすくしています。

## チャット / Chat

ほとんどの場合、チームに対応するSlackチャンネルを作成しています。 [github.com/wantedly/dev](https://github.com/wantedly/dev) (internal) に一覧があります。

チームメンバー以外の人も出入り自由です。\
イシューを立てるGitHubリポジトリなどチームごとの情報もピンされているので、まずはSlackを見てみましょう。

## ミーティング / Meeting

チーム内のミーティングはチームごとに決めていますが、全体で情報共有・意思決定するための定期的なミーティングがいくつかあるので、ここでは関心ごとにそれを紹介します。

### 定例ミーティング / Periodic Meeting

#### 全体像

<img src="/files/YXeechhboTSHbmD3TWI9" alt="" width="700">

#### 主な会議体

次のような会議体で構成されています。

* Dev Leaders Sync Up
  * VP Office から Dev Branch のリーダー（SL / CL / TL）への情報拡散、リーダーから VP Office への情報集約、リーダー同士での議論を行うミーティング。
  * Squad / Chapter に閉じない影響のあるエンジニアリングの情報共有・意思決定を行う。
* Dev Manager Sync Up
  * Dev Branch のマネージャーによる組織的な情報共有・意思決定を行うミーティング。
* Tech Lunch
  * 開発チームのメンバー同士で知見を共有するミーティング。
  * 「開発チーム全体としての技術力・課題解決能力が高められる & その状態が維持できる状態」を実現することを目的としている。
  * Dev Branch の人が基本の参加者であるが、他の Branch の人も自由に参加できる場である。

その他、全社的な会議体として以下があり、連動している。

* Lunch Meeting
  * 経営会議。CEO と各 Branch のリーダーが参加するミーティング。
* Demo Day
  * プロダクトに関わる人に限らず、各プロジェクトの進捗や成果を社内で共有する場。
  * 事業や Squad の状況・取り組みを共有し、組織の透明性を高めるとともに、組織・チーム同士の相互理解につなげる時間でもある。

### アドホックミーティング / Ad-hoc Meeting

定常的に発生する仕事は定例ミーティングなどで処理されることが望ましいですが、アドホックな仕事が発生した時は、アドホックなミーティングを行うこともあるでしょう。そういった時は、Google Calendar を利用して招待を送ります。コンテキストが共有されていない場合は、予定の説明に情報を付け加えるなどすると親切でしょう。

※ リファクタリングの好機：アドホックミーティングが増えてきたら、リファクタリングを考える時かもしれません。定例ミーティングを増やしたり、あるいは既存の定例ミーティングの責務を見直したりしてみましょう。


# ドキュメンテーション(未執筆)


# カレンダー

## おすすめの設定

Google Calendar はそのままでも便利ですが、うまく使うことで仕事の進めやすさは飛躍的に向上します。 これは自分のためになるのはもちろん、自分と一緒に働く他のメンバー・リーダーが気持ちよく働くためにも重要になることがあります。 ここで紹介されている設定は一通り目を通しておいて、できるだけ設定しておくといいでしょう。

### Slackに連携して予定開始の通知を受け取る

プログラミングなど集中して仕事をしているときは、時間を忘れて作業してしまって予定に遅れてしまうことがあるかもしれません。 Google Calendar の Slack App では、予定開始前に Slack 上でリマインドを受け取ることができるようになります。 予定開始のN分前(好みによりますが、1\~5分程度)にリマインドの設定をすることで、予定に遅れるリスクはかなり低減できるでしょう。

* [通知設定を変更する - Google Calendar と Slack を連携させる | Slack](https://slack.com/intl/ja-jp/help/articles/206329808-Google-Calendar-%E3%81%A8-Slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B#u36890u30693u35373u23450u12434u22793u26356u12377u12427)

### Slack上で「会議中」であることがわかるようにする

これも Slack App の機能です。Google Calendar 上で予定がある時間帯に Slack のステータスを `会議中` にすることができます。 メンションするときに名前の横に `会議中` と出るようになるため、わざわざカレンダーを見に行かずとも「今この人は会議中だからレスポンスが遅いかもな」と認識させることができます。

* [Slack のステータスを自動更新する - Google Calendar と Slack を連携させる | Slack](https://slack.com/intl/ja-jp/help/articles/206329808-Google-Calendar-%E3%81%A8-Slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B#slack-u12398u12473u12486u12540u12479u12473u12434u26356u26032u12377u12427)

### 予定のデフォルト権限設定を変更して調整を円滑にする

Google Calendar のデフォルトでは、予定の時間変更は予定の Organizer（作った人）にしかできないようになっています。 Web 上で `設定(Settings)` > `予定の設定(Event Settings)` から `デフォルトのゲストの権限(Default guests permissions)` を変更できるので、これを `予定を変更する(Modify event)` にしておきましょう。 リスケジュールが必要になったときのテキスト or 口頭コミュニケーションを減らすことができます。

* [予定のプライバシー設定を変更する - パソコン - カレンダー ヘルプ](https://support.google.com/calendar/answer/34580?hl=ja\&co=GENIE.Platform%3DDesktop)

### 他の人のカレンダーや共有カレンダーを subscribe する

コミュニケーションを取ることが多い相手、同じチームのメンバーやリーダーの予定を知っておくことで、仕事を進めやすくなる場合があります。 予定を確認する頻度が高い相手がいる場合、その人のカレンダーを subscribe して自分のカレンダー上に表示しておくと便利かもしれません。

* [他のユーザーのカレンダーに登録する - Google Workspace ラーニング センター](https://support.google.com/calendar/answer/37100?hl=ja)

ウォンテッドリー社内には個人に紐付かない共有のカレンダーがいくつかあります（後述）。 社内に限らない便利な共有カレンダー（e.g. 日本の休日）なども存在します。 そういった共有カレンダーも subscribe しておくと便利でしょう。

### オフィスの部屋を確保する

オフィスの部屋を使いたい時は、Googleカレンダーから部屋の予約を入れる事ができます。

![](/files/Jy8Zhz4t24B6bYuGRrWI)

Slack上の任意のチャンネルで「会議室」と送信すると、bot が各部屋の場所がわかる地図を返してくれます。

## 主要な共有カレンダー

登録することでコミュニケーションが円滑になるカレンダー一覧です。 自分のアカウントに連携することで便利に利用できます。 カレンダーごとの運用の変化をこのドキュメントに記載するまでにリードタイムが生じることがあるので最新の運用については各参考リンクを確認してください。

### リリースカレンダー (internal)

いつどのようなリリースがが予定されているかを全社で共有するためのカレンダーです。 下の内容が記述されます。

* Product のリリース
* キャンペーン施策の開始/終了

全社で頻繁に更新されるものであるため更新は Google Sheets に記載することで行います。 詳しくは参考の情報を確認してください。

#### 参考リンク

* [カレンダー本体 (internal)](https://calendar.google.com/calendar/u/0/embed?src=c_i3hh7jplcruhig0ie3ok55mdls@group.calendar.google.com)
* [Google Slides でのアナウンス (internal)](https://docs.google.com/presentation/d/10u57hGGIF0vDU-95wzhTEnhqzTU0QzLao8bfTdf2M4Y/edit)
* [ソースとなる Google Sheets (internal)](https://docs.google.com/spreadsheets/d/1lhDht55EXrgA75qKVPEVWyRU7N7McmYCSYes8meqW3c/edit)

### Dev Meetups/Workshops/Trainings (internal)

ウォンテッドリーのメンバーが運営に関わる技術的な勉強会やミートアップのイベントが共有されるカレンダーです。 勉強会・輪読会を開催する際はこのカレンダーに記載をお願いします。 下のようなものが入っています。

* 社内の人しか参加できない勉強会
* 社内参加がメインで社外の人も参加可能である勉強会
* ウォンテッドリーオーナーではないが運営に協力しているイベント

#### 参考リンク

* [カレンダー本体 (internal)](https://calendar.google.com/calendar/embed?src=wantedly.com_vji87sspsee32pt58l6cgsk7d8%40group.calendar.google.com)
* Slack: [#dev\_meetups\_calendar (internal)](https://wantedly.slack.com/archives/CLM7JS7DZ/)


# 障害対応の心構え

## このドキュメントを読む前に

このドキュメントはインシデント発生前に予防として読んでおくことを想定しています。 緊急時は [Incident Response (internal)](https://dev-docs.wantedly.com/incident_response) 及び [#war\_room (internal)](https://app.slack.com/client/T025XTPMG/C93P2S60N) を確認してください。

インシデント対応について [Incident Response (internal)](https://dev-docs.wantedly.com/incident_response) がより網羅的かつ真とするドキュメントです。 このドキュメントでは上記のドキュメントの内容を補足する内容として特に new joiner が認識しておくべき情報を抽出/加筆したものです。 もし上のドキュメントとこの章に食い違いが生じる場合は上のドキュメントを真としてこの章を編集してください。

## このドキュメントの使い方

各チームで new joiner を受け入れるときにこのドキュメントを一律に共有することで共通認識を得ることを目的にします。 このため主に障害対応経験が少ない人に向けた最低限の心構えを共有するものであり障害対応に関する情報を網羅的にまとめたものではありません。

障害対応経験者は障害対応を誰かに任せる場合にはこのドキュメントの内容を把握しているかどうかを確認してください。

## そもそもの開発において

本番環境にバグを入れる可能性をゼロにはできないということを受け入れましょう。 もちろん良い設計、テスト、レビューなどでその可能性を下げることが望ましいですが完全にゼロにすることは現実的に不可能です。 また、完全にバグがゼロという状況はサービス運営企業としてはリスクが取れていなさすぎるとも考えられます。

ウォンテッドリーは失敗を受け入れる文化を持っているので、バグを出すことを恐れて開発スピードを遅くする必要はありません。 テストやレビューを頑張っても不安が消しきれない場合はバグが生じてもユーザー体験が落ちにくい工夫をすると良いでしょう。 例えば「特定の query param があるときだけ新実装が使われる」「まずは社員にだけリリースされるようにする」「何かあったら古い実装に fallback するようにしておく」などの手法があります。

一人で考えるとリスクをすべて自分で背負ってしまう気持ちになることあるので、そういう場合は周りの人を捕まえて一緒にどうするかを考えると建設的に進むことがあります。 失敗が起こることを受け入れ、チームで助け合いながら開発を進めていきましょう。

## 本番環境に問題が生じた時

まずは過剰反応である可能性を一切忘れて、どんな小さな問題だと思ったとしてもどんな時間であっても大声で人を集めましょう。 自分一人で判断しないことが大事です。 ウォンテッドリーのメンバーは必ず協力してくれます。 問題を過小評価しないことがいちばん大事なので、過大評価することを恐れる必要はありません。 具体例として「深夜にたまたま少し挙動がおかしい可能性に気づいた」というような場合にすでに就寝している自分のリーダーを起こす、というようなことがあっても構いません。 仮にそれが誤報であったとしても「勇気を持ってレポートしたこと」は称賛されるべきです。 繰り返しになりますが過剰反応になること一切恐れず、逆に一切過小反応にならないようにしましょう。

また、誰の Pull Request が原因であったとしても ウォンテッドリーは失敗を受け入れる文化を持っているので隠す必要はありません。 個人の感性で問題ではないかと思ったらまずまわりに協力を求め、そこで判断の精度を上げつつ、必要ならもっとまわりを巻き込んでいきましょう。

基本的には [#war\_room (internal)](https://app.slack.com/client/T025XTPMG/C93P2S60N) でコメントすると人が集まります。 このチャンネルでは任意の投稿に対して通知を受け取るにしている人が多いので簡単に気づいてもらえます。 簡単に気づいてもらえる反面、簡単に注意を引いてしまうので投稿する場合は「とにかく緊急で助けてほしい」「すでに対応に十分な人員がいる」などの情報を共有するようにしましょう。 また、最終手段として [緊急時の連絡先 (internal)](https://github.com/wantedly/infrastructure#緊急時の連絡先) に主要な人に到達できる電話番号が書かれています。

## 対応で考えること

ここでは主に Kubernetes に deploy されているコンポーネントにおける障害の緊急対応方法を記述します。 これに該当しない Mobile アプリなどは修正にかかるリードタイムが長くなるため分単位のタイムロスを考慮する必要がないためです。 Mobile アプリで障害を起こした場合の考え方は後述の「Mobile アプリの場合」で述べます。

### 一つ前の Deployment の状態に戻す

Incident には様々な原因がありえますが、最も多いものの一つに deploy があります。 つまり本番環境に入ったコードの変更に起因するものです。 このような場合は「とにかく早く想定通りに動いていた version に戻す」というアクションが有効です。

```bash
kube prod history
```

この操作でいつどの commit が deploy されたのかを把握することができます。 この中で一つ前のものを deploy してみましょう。 重要なこととしてここの表示され一つ前とはる必ずしも git における一つ前の commit ではないということです。 複数の Pull Request の merge がまとめて deploy された場合にどれが原因かわからないということもありえるので、 「一つ前の commit」ではなく「一つ前の deployment 状態」に戻す操作が有効なケースが多いでしょう。

### 古い commit を deploy し直す

一つ前の deployment にしても問題が継続する場合にはもっと古い version を deploy することを検討しましょう。 コードベースを修正するのにはどうしても数分\~数十分の時間が必要です。 push された任意の commit に対応する docker image が container registry に push されているので基本的には任意のタイミングの commit の状態に 1 分程度で戻すことができます。 コードの修正を行う前にこれが実現可能かを確認しましょう。

```bash
# <commit hash> の状態を deploy する
kube prod deploy <commit hash>
```

これを行う際は「戻しすぎると他の場所が壊れることがある」という点に注意しましょう。 典型例としては「DB migration が入っているため戻すと DB との整合性が取れないので戻せない」「リリース済みで動作するべき他の機能が未リリースに戻ってしまう」などがあります。

また自動 deploy によりバグがもう一度 deploy されてしまう可能性があるので自動 deploy を止める `lock` を行いましょう。

```bash
# production への deploy を 1h 禁止する
kube prod lock 1h
```

### Revert する

バグ修正を試みると想定以上の時間がかかったり新たな問題を生んでしまったりすることもあります。 原因の PR が特定できる場合はその PR の revert ができないかを考えましょう。

基本的には revert の PR の作成は早ければ早いほうが良いです。 古い commit を deploy するという対応が取れない場合、 最速の復旧方法は「Revert の PR を `main` (`master`) に merge せずに deploy する」というアクションになります。 この deploy が可能になるタイミングは Revert を作成してからその PR に対する docker build の作成が完了する時間に依存します。 Revert を過剰に作りすぎることのコストは無視できるので、怪しい PR があったら何よりも早く Revert PR を用意しましょう。

### Mobile アプリの場合

Mobile アプリはストアの審査とユーザーの端末でのアップデートを経て修正が届くため、 サーバーサイドのように deploy を切り戻して分単位で復旧することはできません。 問題が生じた場合は以下のような対応を検討しましょう。

* Feature Flag や API などサーバーサイド側で問題の機能を無効化・緩和できないかを最初に考える
* 段階的リリースの途中であれば、リリースを一時停止して影響を受けるユーザーの拡大を防ぐ
* 修正版のアプリを申請する場合、iOS では優先審査 (expedited review) を申請することで審査時間を短縮できることがある

### 思ったことは何でも言う

障害対応経験が浅いタイミングで実際の障害対応に入ると、 「自分が思っていることはみんなわかっているだろう」と思ってあまり発言をしないこともあるでしょう。 しかし障害対応中は少なからず焦っているので当たり前のことに気づいていないこともあります。思ったことは何でも発言していきましょう。

## 対応後

### Post Mortem

インシデント対応後は post-mortem を書きます。 詳しくは[ポストモーテムの取り組み](/fields/dev-process/post-mortems)を確認してください。

## この文化を維持するために必要なこと

ポストモーテムの取り組みが長期間続いていることもあり、近年ウォンテッドリーではますます障害発生がレアな事象になりつつあります。 この状況下でこの障害対応に対する心構えの文化を維持するためにはこれから障害に関わる人にこの文化を伝播していく姿勢が重要です。 ここではいくつかの例とともにこの文化を維持する方法について述べます。

### これからオンコールを担当する人に掛ける言葉

これからオンコールを担当する人にはこのドキュメントを読んでもらいましょう。 そしてこのドキュメントで示されているポイントを自分の言葉でも説明してください。 その際に「自分も xxx のようなケースで深夜に人を起こしたことがある」「昔大騒ぎしたけど誤報で笑い話になったことがある」 というような自分のエピソードを話すと良いでしょう。 もし自身にそういった経験がなければ他のメンバーに話を聞いてみると良いかもしれません。

### 障害を起こしてしまった人に掛ける言葉

障害を起こしてしまった人は誰よりも落ち込みます。 しっかりとした再発防止は多少時間が経ってからでもできますが、 本人が感じる過度な自責の念と緊張感の払拭は即時でないとできないことがあります。 はじめて障害を起こしてしまった人には「はじめての障害おめでとう」と冗談をいうくらいでも丁度よいです。

### 誤報を出した人に掛ける言葉

まずは報告してくれたことに対する感謝を伝えましょう。 加えて「誤報で良かった」ことを伝えましょう。 スタンプなどで「よかった」「ありがとうございます」などを押すだけでも 報告者の心理的障壁が下がります。

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570), [#post\_mortem](https://wantedly.slack.com/archives/C027KFZCU5V)

#### もっと知りたい

* [ポストモーテムの取り組み](/fields/dev-process/post-mortems)
* [wantedly/post-mortems (internal)](https://github.com/wantedly/post-mortems)
* [仕事の進め方 (internal)](https://github.com/wantedly/dev/blob/master/docs/beginners/work_process.md)
* [インシデント対応時チートシート (internal)](https://github.com/wantedly/infrastructure/blob/master/docs/cheatsheet.md)
* [緊急時の連絡先 (internal)](https://github.com/wantedly/infrastructure#緊急時の連絡先)
* [Incident Response (internal)](https://dev-docs.wantedly.com/incident_response)


# 効率的な社内知識の調べ方

ウォンテッドリーには大量の社内ドメイン知識が存在します。 それらは業務中頻繁に遭遇するため、はじめに効率の良いキャッチアップ手順を覚えておくとよいでしょう。

## 4 ステップで調べる

社内で何かを調べる際は以下の 4 ステップで調べましょう

1. [Handbook](https://docs.wantedly.dev/) で調べる
2. [Backstage](https://backstage.internal.wantedly.com) で調べる
3. AI に聞く
4. Slack で聞く・過去の GitHub Issue/PR で調べる

### 社内向け

現在 Handbook に対する社内フィードバックを積極的に集めています。「欲しい情報が見つからなかった」「知りたいことが知れて便利だった」などは是非[こちらの Discussion (internal) ](https://github.com/wantedly/dev/discussions/1923)にコメントしてください。

## 詳しく

### 重要なことは大体 Handbook に書いてある

Handbook は公開前提で書かれていたり、[元々オンボーディング資料として作られていた](https://docs.wantedly.dev/preface)こともあって社内で一番読みやすいドキュメンテーションになっています。 そのため、New Joiner のオンボーディングや専門外の領域について調べる際はまず初めに Handbook で調べると良いでしょう。 また、検索だけでなく目次をざっと眺めて「こういう概念あるんだ〜」のインデックスを頭に作っておくことも重要です。

![検索窓の場所の画像](/files/Y1jbeSfbgwP4UymaNvB7)

ただし特定のチームやマイクロサービスに紐づく詳細情報は Handbook ではなく、個々のリポジトリに記載する運用なので、それらは後述の Backstage を用いて検索してください。

### 詳細情報は Backstage で調べる

Backstage は複数の GitHub リポジトリに存在するマークダウンファイルを横断的に検索できるツールです。

![Backstage で検索したときの png](/files/G0FiQZNpwqQltXTlTJJA)

Backstage には大量のドキュメンテーションが保存されています。そのため、検索キーワードさえ知っていれば簡単に目当ての情報を得ることが可能です。

一方で、ページ数が多すぎるがゆえ明確な検索キーワードを持っていないと非常に扱いづらいです。 New Joiner や専門外の領域について調べる際は、まずは Handbook で全体像を掴んだ上で個別のトピックを Backstage で検索すると良いでしょう。

### 検索で見つからなければ AI に聞く

Handbook や Backstage の検索は、明確な検索キーワードを持っていないと目当ての情報にたどり着けないことがあります。 そんなときは AI に調べさせましょう。曖昧な聞き方でも、AI が関連するドキュメントやコードを探して回答してくれます。

調べる対象に応じてツールを使い分けると効率的です。

* 組織横断的に調べたい場合は Devin が向いています。複数のリポジトリやドキュメントを横断して調査できます
* 特定のリポジトリについて調べたい場合は Claude Code をはじめとする各種 AI エージェントツールが向いています。リポジトリのコードやドキュメントを直接読んで回答してくれます

ただし AI の回答は誤りを含むことがあります。重要な判断をする際は、回答内で参照されている Handbook・Backstage・コードなどの一次情報を確認しましょう。

### それでも分からなければ Slack で聞く、過去 Issue を調べる

Handbook・Backstage・AI を利用すればほとんどの調べ物は行えますが、時には目当ての情報にたどり着けないこともあります。 そんなときは Slack で質問をしたり、過去の Issue/PR を探すことで欲しい情報を探しましょう。

![Slack で検索したときの png](/files/H6kZtVrS5KgsN5dkqYuZ)

ただし、次回以降も違う人が Slack で質問したり、過去 Issue/PR を探したりするのは非効率です。 ドキュメンテーション化されていなくて困った情報は積極的に PR を作成しましょう。

#### 話を聞きに行きたい

* Slack: [#devops\_guild](https://wantedly.slack.com/archives/C04P8GL7ZPS)


# 外部発信

## 発信にあたって大切にしていること

ブログ記事や外部登壇は、技術や取り組みを共有する場であると同時に、私たちの価値観や姿勢が伝わる場でもあります。そのため、内容の正確さや分かりやすさに加え、人への向き合い方や表現のあり方を大切にしています。この章は、ブログ記事・外部登壇に共通する、発信時の基本的な考え方です。

### 人の尊厳を前提とした表現を用いる

発信内容は、すべての人の尊厳が守られることを前提として作成します。人物や集団を過度に単純化した比喩や、特定の立場・役割・特性を矮小化する可能性のある表現は、意図せず誤解を生むことがあるため慎重に扱ってください。文章の分かりやすさや印象を高める目的であっても、人を評価・分類するように受け取られ得る表現になっていないかを必ず確認します。

### 多様な読者に配慮した発信を行う

わたしたちの発信は、社内外の多様な背景を持つ読者に届くことを前提としています。経験、役割、文化、価値観の違いによって受け取り方が変わる可能性を考慮し、特定の前提知識や立場に依存しすぎない表現を心がけてください。一部の人にとって読みやすい表現であっても、他の立場の読者にとって違和感や距離を生まないかを意識することが重要です。

### 迷いが生じた場合はレビューを優先する

表現の妥当性について少しでも迷いが生じた場合は、個人の判断で進めず、編集者や関係者に相談してください。外部への発信は個人のアウトプットであると同時に、ウォンテッドリーの会社・組織としてのメッセージでもあります。複数の視点を取り入れて確認することで、発信の品質と安心感を高めることができます。

## Wantedly Engineering Blog

ウォンテッドリーの開発組織に属するメンバーによる技術ブログです。開発者向け技術情報を中心に発信しています。

### 目的

社内外でのナレッジの蓄積と共有を目的としています。日々の開発や運用の中で得られた知見や技術的なチャレンジ、その過程と解決方法を以下の点に留意して公開することで、組織全体の技術力向上を目指します。

* ナレッジの可視化・蓄積
  * 各エンジニアが得た知識や経験をブログとしてストックし、組織内で活用できる形にすることで、全体の生産性を向上させます。
* 社内外への発信
  * 社内だけでなく、広く社外にも技術的な取り組みや知見を発信し、エンジニアリングコミュニティに貢献します。
* 技術的な透明性の向上
  * 技術課題に対するアプローチやプロジェクトの進行状況を公開することで、透明性を高め、他チームや外部の関係者との円滑なコミュニケーションを促進します。

### 手段

技術ブログを効果的に運用し、ナレッジの蓄積と共有を円滑に進めるために、具体的な執筆プロセスやツールの活用方法を定めています。以下に、技術ブログの執筆から公開までの流れを示します。

#### 記事の執筆プロセス

**テーマ選定**

各エンジニアが直面した技術的な課題や解決方法、または新しい技術導入の成功例や失敗例をもとにテーマを選定します。知識だけでなく、知見も価値あるテーマとなります。

**ドラフト作成**

記事の骨子をまとめ、詳細な説明や実例を含めた草案を作成します。読者に分かりやすく、再現可能な内容を意識して書くことが推奨されます。レビューを求める場合は、Googleドキュメントを活用することで、フィードバックを受けやすくなります。

**レビュー**

執筆された記事は、他のエンジニアやプロジェクトメンバーによってレビューされ、技術的な正確さや表現の改善を図ります。

**公開**

レビューを通過した記事は技術ブログとして公開します。公開された記事は、執筆者自身が責任を持ち、最終的なアウトプットを確認し、表示崩れや不適切な表現がないことをチェックします。

### 執筆ガイドライン

技術ブログの記事執筆において、読者にとって読みやすく、信頼性のある情報を提供するために、以下のガイドラインに従ってください。技術ブログ以外にも、技術イベントの集客に用いるページ作成にもこのガイドラインを適用します。

#### 文章構成

* パラグラフを意識した構成
  * １つのテーマやポイントごとに適切な長さのパラグラフを使用し、文章をまとめてください。一般的なブログのような一文ごとの改行や、不要な連続改行は避け、知的かつプロフェッショナルな文体を維持しましょう。

#### 言葉遣い

* 正式呼称の使用
  * 製品名、サービス名、技術用語など、正式な呼称を使用してください。略語や口語表現は避け、必要な場合には初回に説明を加えた上で使用します。
* 専門用語の説明
  * 専門的な用語や技術的な概念は、読み手に分かりやすいように説明を加えるか、リンクを提供することで補足してください。特に社外向けの記事では、業界外の読者にも理解できるよう工夫しましょう。
* 絵文字・顔文字の禁止
  * 文章中で絵文字や顔文字の使用は禁止です。技術ブログでは、内容の正確性とプロフェッショナルな印象を大切にするため、こうしたカジュアルな表現は避けましょう。
* 「！」や「？」の多様を禁止
  * 感嘆符（「！」）や疑問符（「？」）の過度な使用は避け、文章全体のトーンが落ち着いたものとなるようにします。必要に応じてこれらを使う場合も、1回までの使用に留め、過剰な強調を控えてください。

#### 表記の統一

* 文体の一貫性
  * 書き手ごとの文体の違いを最小限に抑えるため、敬体（です・ます調）と常体（である調）の混在を避け、一貫した文体を維持します。公開するブログの目的に応じて適切なスタイルを選択してください。
* 日本語と英語の使い分け
  * 日本語の記事の場合、固有名詞や技術用語などを除き、基本的に日本語を使用してください。必要に応じて英語を併記する場合は、カッコ書きや注釈を使い、読みやすさに配慮します。

#### スタイル

* 箇条書きやリストの使用
  * 複数のポイントを示す際は、適切に箇条書きや番号付きリストを使用し、読み手が情報を整理しやすいようにしましょう。
* 画像やコードブロックの挿入
  * 技術的な内容を視覚的に補足するために、必要に応じて画像やコードブロックを挿入してください。コードブロックは正確であり、適切なハイライトやコメントを含めて、読み手にとって明瞭にすることが求められます。
* 記事にふさわしいカバー画像の使用
  * 記事の内容を反映した適切なカバー画像を選定してください。読者に対する視覚的なインパクトを考慮し、内容との関連性が明確な画像を用いることが重要です。奇をてらった画像の使用は避け、記事内容に沿った画像を使用してください。

#### 編集と校正

* レビューを受ける
  * 執筆した記事は、必ず他のメンバーにレビューを依頼し、内容の正確性や表現の適切さを確認してください。
* フィードバックの反映
  * 受け取ったフィードバックは積極的に反映し、記事を修正・改善してから最終的に公開します。

## 外部登壇

### 目的

外部登壇は、技術的な取り組みや知見を広くエンジニアリングコミュニティに共有し、組織の技術的信頼性を高めるための重要な活動です。以下の目的を達成するために外部登壇を推奨します。

* 技術の普及と共有
  * 社内で培った技術的知見や経験を社外に発信し、エンジニアリングコミュニティ全体に貢献する。
* ブランド価値の向上
  * 開発組織の技術力を広く認知させ、エンジニア採用やブランド力の向上に繋げる。
* 個人の成長
  * 登壇を通じて、個々のエンジニアのスキルアップや業界内でのプレゼンスを高める。

### 手段

外部登壇を効果的に行うため、以下の手段を用いて準備を進めます。

* ターゲットの明確化
  * 登壇するイベントやカンファレンスの参加者層を明確にし、その対象に合ったテーマや内容を選定します。登壇テーマは、自社の技術的アピールポイントや、コミュニティに有益な情報を軸に決定します。
* 社内リソースの活用
  * 社内で蓄積された知識や成功事例を活用し、社内メンバーの知見を最大限に活かしたプレゼンテーションを作成します。既存の技術ブログやドキュメントを参考にしながら、最新情報やインサイトを加えて発信します。
* 定期的な社内練習
  * 社内で登壇内容のリハーサルを行い、フィードバックをもらいながら質を高めます。特に初めて登壇するメンバーには、先輩エンジニアからのアドバイスやサポートを受けることが推奨されます。社内で開催される Tech Lunch や Demo Day を活用ください。

### 登壇資料の作成プロセス

#### テーマ選定

登壇先のイベントやカンファレンスの主旨と参加者の興味を考慮し、最も効果的なテーマを選定します。自社の強みを活かし、コミュニティのトレンドに合った内容を選ぶことが重要です。また、日々のプロジェクトにおいて「知見としてまとめたおきたい」と思ったらすぐに資料を作成しストックすることを推奨します。

#### スライド構成

序盤に資料の目的や習得して欲しいことを明記しましょう。技術力やトピックの重要性を明確に伝えます。各スライドは視覚的に魅力的で、かつ簡潔であることが求められます。箇条書きに頼らずに文章を用いましょう。文字数を減らし、ビジュアルや図をバランスよく挿入すると理解しやすい資料になります。スライド内容はストーリー性を持たせ、最初から最後まで一貫したメッセージを伝えることを意識します。

#### レビューと練習

スライドが完成したら、社内でのレビューを行います。フィードバックを基に修正を加えた後、練習を通じて内容や時間配分を調整します。

#### 最終確認

最後に、発表資料をチェックし、内容の正確性、表現の適切さ、ビジュアルの一貫性などを確認します。登壇先の主催者やイベント運営者とも最終的な確認を行い、登壇準備を完了します。

### ガイドライン

#### 資料のフォーマット

ウォンテッドリー株式会社の一員として登壇する際は、会社指定のフォーマットを使用してください。

#### 技術的正確さ

発表内容は、技術的な正確性を第一に重視します。社内レビューを必ず受け、誤りや誤解を招く表現がないよう注意します。

#### 著作権と引用の遵守

使用する図表やデータには、著作権や使用許諾を確認した上で正しく引用します。必要に応じて出典を明記し、他者の成果物を正しく扱います。

#### 時間配分

発表の時間配分に気を配り、リハーサルを通じて時間内に収める練習を行います。発表時間に余裕を持たせ、Q\&Aセッションの準備も整えます。

#### 機微情報やプロダクトに関する数字

社内のPRガイドラインに準拠します。各プロダクトのKPIの具体的な数字や、機密情報やインサイダー情報は掲載禁止です。

#### 登壇資料の公開

個人で SpeakerDeck にアカウントを作成して公開しても良いです。公開する際には、Upload後のフォント表示、URLには注意が必要です。意図した表示かどうか自身で確認してください。

## WANTEDLY TECH BOOK

ウォンテッドリーでは、技術書の同人誌イベントである「技術書典」に、社内から有志を募りサークル「Wantedly執筆部」として毎回参加し、WANTEDLY TECH BOOK を作成・頒布しています。 また、ウォンテッドリーが外部カンファレンスに協賛する際にも、スポンサーブースにて WANTEDLY TECH BOOK を頒布することがあります。

WANTEDLY TECH BOOK は、分野や Squad/Chapter を問わず、普段の業務を通して得られた知見を、社内だけでなくコミュニティ全体に還元することで技術に貢献したいというモチベーションから作られています。

### 過去の TECH BOOK バックナンバー

| 書籍リンク                                                                                                                                | 初出イベント名  |
| ------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| [WANTEDLY TECH BOOK 14：Wantedly執筆部](https://techbookfest.org/product/xzCdC4Hi5YhUMppNtCQzJ9?productVariantID=wNuKYU6EWEiYuiqQEa03KL) | 技術書典16   |
| [WANTEDLY TECH BOOK 13：Wantedly執筆部](https://techbookfest.org/product/h7dbA2JgvZJZteH84rfk4P?productVariantID=kepLaewLt8EqAmrRFA1UyT) | 技術書典15   |
| [WANTEDLY TECHBOOK 12：Wantedly執筆部](https://techbookfest.org/product/r2kTBWxtVGnxVGfQZSAJZn?productVariantID=fr536YDm19JV5H0aMqm3bm)  | 技術書典14   |
| [WANTEDLY TECH BOOK 11：Wantedly執筆部](https://techbookfest.org/product/wfphEZB7nQwrVLLqnqATV8?productVariantID=azFgRdNaXZ7BmqB00aDMdJ) | 技術書典13   |
| [WANTEDLY TECH BOOK X：Wantedly執筆部](https://techbookfest.org/product/4976690428116992?productVariantID=6562079022514176)              | 技術書典10   |
| [WANTEDLY TECH BOOK 9：Wantedly執筆部](https://techbookfest.org/product/6095813658804224?productVariantID=5549265715724288)              | 技術書典9    |
| [WANTEDLY TECH BOOK 8（電子版のみ）：Wantedly執筆部](https://techbookfest.org/product/6392484666015744?productVariantID=6065402454999040)       | 技術書典 応援祭 |
| [WANTEDLY TECH BOOK 7：Wantedly執筆部](https://techbookfest.org/product/6565153113899008?productVariantID=6618001386242048)              | 技術書典7    |
| [WANTEDLY TECH BOOK 6：Wantedly執筆部](https://techbookfest.org/product/6192470345908224?productVariantID=4840930079145984)              | 技術書典6    |
| [WANTEDLY TECH BOOK 5：Wantedly執筆部](https://techbookfest.org/product/6364423656570880?productVariantID=4912504165629952)              | 技術書典5    |
| [WANTEDLY TECH BOOK 4：Wantedly執筆部](https://techbookfest.org/product/258600001?productVariantID=4573344422690816)                     | 技術書典4    |
| [WANTEDLY TECH BOOK 3：Wantedly執筆部](https://techbookfest.org/product/248650004?productVariantID=4601102880210944)                     | 技術書典3    |
| [WANTEDLY TECH BOOK 2（電子版のみ）：Wantedly執筆部](https://techbookfest.org/product/260570028?productVariantID=5290874091077632)              | 技術書典2    |
| [WANTEDLY TECH BOOK （電子版のみ）：Wantedly執筆部](https://techbookfest.org/product/254580018?productVariantID=6051289997770752)               | 技術書典     |

最新刊を除く過去の TECH BOOK は、以下のリンクからも無料でダウンロードできます。

* [📚 過去の TechBook](https://drive.google.com/drive/folders/1sY2eF93rTaNkQeqIgtnNZntXpUueQ5DZ)

### 話を聞きに行きたい

* Slack: [#tech\_blog](https://wantedly.slack.com/archives/C92QZFNM7)

### もっと知りたい

* [物理本の作成と技術書典への参加の歴史 (internal)](https://wantedly.atlassian.net/wiki/spaces/WHR/pages/3034284033)
* [TECHBOOK作成と技術書典参加の流れ (internal)](https://wantedly.atlassian.net/wiki/spaces/WHR/pages/3099328513)


# Apps


# アプリを提供するプラットフォーム

Wantedlyのサービスを提供するアプリをWebアプリとネイティブアプリの2つに大別し、それぞれの特徴とリリース戦略を説明します。

## Webアプリ

Webアプリは、Webブラウザ上で利用できるアプリです。

デスクトップとモバイルのデザインがあり、閲覧するデバイスや画面幅によってレスポンシブに切り替えます。 一部機能はデスクトップからのみ利用を想定しているためモバイル対応（レスポンシブ等）をしていないケースがあります。 e.g.) 管理画面はデスクトップでの利用想定であるためレスポンシブ対応をしていない

### リリース戦略

Webアプリは、デプロイ容易性が高い特徴があります。

リリースしたい機能や施策ごとにPull Requestを作成し、Pull Requestをマージすることで即座にデプロイされます。 Pull Requestのマージをリリース決定判断とすると、リリース決定判断からユーザーに届くまでのリードタイムは、15分前後です。

デプロイ容易性を活かし、Canary Releaseによりバグの影響範囲を最小限に抑えたり、デプロイのロールバックを行うことでユーザー影響を素早く最小限に抑えることができます。

## ネイティブアプリ

ネイティブアプリは、Webブラウザを介さず、OSのようなプラットフォーム上でネイティブに動作するアプリです。

代表例として、iOSとAndroid向けのネイティブアプリがあります。 他にもiPadやmacOS、Windowsのデスクトップアプリもネイティブアプリに該当します。

プラットフォームごとに最適なデザインが異なります。プラットフォームを横断してデザインする際には、各プラットフォームの特徴を捉える必要があります。

### リリース戦略

ネイティブアプリは、ユーザーがアプリをインストールしてユーザーが利用可能となるため、アプリを配信する必要があります。 基本的にApp StoreやGoogle Playといったプラットフォーマーのストアへ配信し、プラットフォーマーの審査を通ったらユーザーが利用可能となります。

プラットフォーマーの審査は、半日から2日程度かかる場合もあり、コントロールできません。 そのため、リリース決定判断からユーザーに届くまでのリードタイムが長くなります。

ストアでアプリをリリースしても、ユーザーがアップデートをしない限り、新しいバージョンがユーザーに届きません。 一度ユーザーに届いてしまったバグは、バグを修正したアップデートをユーザーがインストールしない限り、ユーザーの手元に残り続けます。

こういった特徴から、ネイティブアプリのリリースは、品質を重要視します。 そのため、リリースごとに1週間程度の準備期間を設けています。 QAの実施期間、デザイナーによる品質確認、QAバグ対応やストアレビュー対応などが含まれます。

## Webアプリとネイティブアプリの違い

ネイティブアプリは、デバイスのハードウェアを扱えたり、プラットフォームのAPIを活用できるため、Webアプリに比べるとできることがかなり広いです。 ただし、AppleやGoogleといったプラットフォーマーの制限を受けます。例えば支払いが発生する場合、プラットフォーマーの手数料が取られるケースがあります。

ユーザーがネイティブアプリを使うには、アプリをインストールするという手間が必要です。 また、ユーザーに新機能を使ってもらうには、アプリをユーザーにアップデートしてもらう必要があります。 Webアプリでは、サーバーサイドの変更でアップデートを提供できます。

ネイティブアプリは、インストールする反面、一度インストールすればユーザーがアクセスしやすいです。 Push通知のような定期的にユーザーを引き付ける要素もあり、Webアプリよりもユーザーエンゲージメントが高い傾向があります。


# デザインシステム入門

デザインの構造を正しく捉えることは、UI の実装を専門にしているかどうかを問わず、**開発生産性が高く、ユーザーにとっても使いやすい実装を実現する**ための重要なポイントです。 この章では、Wantedly におけるデザインシステムの構成、そして UI を実装する上で基礎となる Wantedly の UI デザインシステムの概念と考え方について解説します。

なお、この記事は[ノンデザイナーズ・Wantedly デザインシステム完全理解ペーパー](https://www.wantedly.com/companies/wantedly/post_articles/395772)を元に書いています。

## Agenda

* Wantedly のデザインシステムの全体像と UI デザインシステム
* UI デザインシステムは何であるか/何でないか
* UI デザインシステムの構成要素
* UI デザインシステムを利用して UI コンポーネントを組み立てる
* その他覚えておくと良いこと

## Wantedly のデザインシステムの全体像と UI デザインシステム

ウォンテッドリーには、 "Graphic Standard" と "UI デザインシステム" というデザインに関する 2 つの基準があります。

* [Graphic Standard (internal)](https://www.figma.com/file/94aqAiQ5ZWq4CkgYV07r2J/ForDesigner): デザインの一貫性を保つ（らしさ を表現する）、デザインのガイドライン
  * 価値観と原則
  * ビジュアルガイドライン: ブランドカラー, カラーパレット, フォント, ...
* UI デザインシステム: UI や体験の一貫性を保つ、（実装に準じた）概念と UI の設計
  * スタイルガイド
  * Foundation(デザインパラメタ, デザイントークン): Elevation, テキスト(サイズ, 行の高さ), ...
  * Component: ボタン, テキストフィールド, ...

![デザインシステムが加速させるプロダクト開発 p12 (Yoshinori Kawasaki, 2019) https://speakerdeck.com/kawasy/design-system-and-scalable-product-development?slide=12](/files/4IiNaBYA5i1XcmI1SjJA)

UI デザインシステムは、特定のプラットフォームや技術に依存しない概念です。 UI デザインシステムの実装として、「デザイナー向けの実装（Figma）」「Web 向けの実装（React）」「iOS 向けの実装」「Android 向けの実装」などが存在します。

特定の UI デザインシステムを実装したライブラリを指して "(UI)デザインシステム" というのは誤りです。 誤解が発生しないよう、正しい表現を使うように心がけましょう。

![](/files/F0rCCFtek9BH1GND0sLI)

## UI デザインシステムは何であるか/何でないか

### これは何？

Wantedly の UI デザインシステムは「Wantedly の UI をデザインする上での共通の考え方とツール＆アセット」と定義されています。

Wantedly らしい UI をデザインするための共通の考え方とツール＆アセットを提供することにより、一貫した表現で、基本的なユーザビリティを備えた UI を効率的に生み出すことを目標としています。

具体的には、以下の 4 つが目的として挙げられています。

* ブランド表現（Wantedly としての見た目と振る舞い）の一貫性を保つ
* ベーシックなユーザビリティの担保
* デザインアウトプットの効率化
  * 細かい造形で悩まず、プロダクトとして大切な体験にフォーカスできるように
  * 複数のプロダクトをまたいでも、共通の考え方で対応できるように
* エンジニアとのフロントエンド開発、コミュニケーション、メンテナンスの効率化

エンジニア視点では、共通している考え方などを知っておくことで、デザイナーとのコミュニケーションを円滑に行う（あるいは省略する）ことが可能になります。 結果として、開発速度やアプリケーション実装のメンテナンス性向上に寄与します。

### 何ではない？

エンジニアとしてはどうしても実装レベルの話、たとえば「共通 UI コンポーネントライブラリ」のようなものを想像してしまいがちです。 しかし、Wantedly の UI デザインシステムはあくまでも「共通の考え方とツール＆アセット」であることに注意が必要です。 特定のプラットフォーム向けの実装のことを指して UI デザインシステムというのは誤りです。(e.g. React 向けのコンポーネントライブラリのことだけを指して UI デザインシステムというのは誤り)

UI デザインシステムの目的の一つに「エンジニアとのフロントエンド開発、コミュニケーション、メンテナンスの効率化」とありました。 これを高いレベルで実現するためにも、エンジニアが利用する実装（React や Swift などによる実装）がデザイン側実装と同じ抽象度であることが重要です。

## UI デザインシステムの構成

UI デザインシステムは、複数のレイヤーが積み重なって構成されます。以下のレイヤーがあります。

* UI コンポーネントを構成するためのデザイン最小単位である「Foundation」
* Foundation の組み合わせからなる「Surface」
* 1 つ以上の Surface の組み合わせからなる UI コンポーネント

![https://www.figma.com/file/94aqAiQ5ZWq4CkgYV07r2J/For-Designer?node-id=40%3A76 (internal)](/files/A2rDAcZt7foG6Oo88H3n)

次の節から、それぞれの要素を見ていきます。

### Foundation - UI デザインシステムの最小単位

UI デザインシステムの最小単位として、Foundation が定義されています。 [デザイントークン](https://blog.adobe.com/jp/publish/2021/03/29/cc-web-what-are-design-tokens)と呼ばれるものと同義です。

Foundation には以下の 10 の要素が存在します

* Graphic Standard をもとに定義されているもの
  * Color Palette
  * Text
  * Icon
* プロダクト開発上必要になるもの
  * Layout Unit
  * Responsive
  * Shape
  * Elevation
  * Dimming
  * Reaction
  * Basic Easings

順に紹介します。

#### Color Palatte

* Wantedly のデザイン上で利用されるカラーパレットです
  * ブランドカラーと統一性のある表現ができる色のセットです
* blue, purple...のような色の種別と、blue400, blue500...のように色ごとのバリエーションが定義されています
  * Wantedly の青は blue400 です
* whiteAlpha100, blackAlpha800 など半透明の白と黒は UI 上で頻繁に利用されます
  * グレースケールのテキストやアイコン、区切り線の色は黒または白の濃度で指定することで、背景色に対して常にコントラストを維持することができるためです

#### Text

* 文字の大きさ、太さ、フォント、行の高さなど Typography に関するパラメタのセットです
  * 忘れがちですが、太さも Text の定義に含まれます
* つかみ 1(catch1), 見出し 3(headline3), 本文 1(body1) など、複数のテキストのスタイルが定義されています
* 文字種 (Alphanumeric or CJK)、 PC or Mobile(Web or iOS or Android) でパラメタが変化したり、プラットフォーム固有のパラメタが存在することがあります

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=0%3A2868 (internal)](/files/2V8hJw0ke1VlkEfRHkBs)

#### Icon

* Wantedly のプロダクト上で利用されるアイコンです
* Graphic Standard では実際に利用されるアイコン集と、アイコンを作るためのスタイルガイドが定義されています

![https://www.figma.com/design/VxoGPQPihKXA9dPsU8sqSz/UI-Elements?node-id=26604-2514\&t=8TQS6pP1s5MLAJcd-11 (internal)](/files/pr07QyZIP3Vusud6fA6K)

#### Layout Unit

* レイアウトは基本的に 8px を基本単位として、その倍数で構成されます
* 場合によっては 4px の倍数が使われることもあります（sub unit と呼ばれます）
* 逆に言うと、小数や奇数を CSS に書いていた場合は、何か（解釈か元のデザイン）が間違っている可能性を疑いましょう。デザイナーとコミュニケーションを取ることを推奨します。

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=0%3A2232 (internal)](/files/vlOsr0EcKnY8N5gj6r4V)

#### Responsive

* レスポンシブ対応における breakpoint を定義しています
* Mobile（縦向きスマホ）、Tablet（縦向きタブレットや横向きスマホ）、Laptop（横向きタブレットや PC）の 3 種類を利用することが多いでしょう

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=0%3A2416 (internal)](/files/TcqQNKqbJE4izh9yZeSC)

#### Shape

* Surface（後述）の形状を定義しています
* R0, R4, R16 のように角丸の半径を表します
* 円形、またはピル型の形状を表現したい場合は ROUND を指定します

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=0%3A2571 (internal)](/files/uBegUfy8gfFOi5VpkCPn)

#### Elevation

* Z 軸方向の高さを shadow で定義しています
* [Material Design における Elevation](https://material.io/design/environment/elevation.html)と同じものと思って問題ないです
* 各レベルの影は計算によって求められます

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=1438%3A0 (internal)](/files/OxxHdD47tlVrd0qyfRvx)

#### Dimming

* Z 軸方向の高さを、裏側の UI コンポーネントに半透明のレイヤをかぶせることで表現します
* アラートダイアログのような、ページ内の上位のレイヤに表示されるモーダルな UI コンポーネントで利用されます

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=0%3A2551 (internal)](/files/5hMkzW6V5RRoN1O0xxff)

#### Reaction

* ユーザー操作の UI コンポーネントのリアクションの種類とその方法を定義しています
* リアクションに通常状態（Normal）に加え、Hover, Pressed, Focused, Disabled の 5 種類が存在します
* ベーシックなリアクション方法は「色のオーバーレイ」「Elevation」「色のオーバーレイ + Elevation」の 3 種類が存在します
* ボタンなど小さい Surface には「色のオーバーレイ + Elevation」、TextField など大きいものには「Elevation のみ」というように、多くの場合は Surface の大きさで決まります
  * UI コンポーネントや利用箇所によってはリアクション方法がオーバーライドされることがあります

![https://www.figma.com/file/VxoGPQPihKXA9dPsU8sqSz/UIElements?node-id=0%3A2182 (internal)](/files/wpJYuFLsHt9vXjz7xH7E)

#### Basic Easings

* コンポーネントの状態が別の状態に変化するとき、時間に対してどのように変化していくかを定義します
  * CSS だと transition プロパティなどに渡す Easing Function に相当します
* ユーザー操作が起点（direct）かそうじゃないか（indirect）や遷移にかかる時間によって利用すべき Easing Function が決まっています

![https://www.figma.com/file/94aqAiQ5ZWq4CkgYV07r2J/For-Designer?node-id=136%3A9341 (internal)](/files/txt1rgHF5TCtgVBoHHtQ)

### Surface - あらゆる UI コンポーネントの基礎

前述した Foundation のうち、Shape, Elevation, Reaction と Background Color(背景色) の 4 値を組み合わせることで、「色がついて、ユーザーの操作に反応できる板」を作ることができます。これを Surface と呼び、あらゆるコンポーネントの基礎となります。

たとえば Wantedly Visit で使われるプライマリのボタンであれば、以下の 4 値から構成される Surface が基本で、その上に白い文字（Text は button）が乗っています。

* Shape: ROUND
* Elevation: 4
* Background: VisitGrad（Wantedly Visit のプロダクトカラー）
* Reaction: Color + Elevation, 白の overlay

![](/files/jFt4Zgsqq4R8ufmagCYc)

UI デザインシステムに定義されているコンポーネントはもちろん、**Wantedly のプロダクトの UI に登場するコンポーネントは原則この Surface から作られています**。 UI を実装するエンジニアは、このことを意識しておくことでデザイン意図を正しく実装することができるでしょう。

![https://www.figma.com/file/94aqAiQ5ZWq4CkgYV07r2J/For-Designer?node-id=136%3A7208 (internal)](/files/EiLuHID6jtiGTv0d3yxv)

#### Theme, Variant

例えばボタンであれば、ユーザーへの訴求の強さに応じて 3 種類の Surface を使いわけます。

![](/files/05H5s2c0jshQVrkQCEYt)

また、同じ強さであってもプロダクトごとに異なる Surface が定義されています。以下は People 用のボタン定義です。

![](/files/G1awbgtaInOTSt0ROynR)

また、そのコンポーネントが配置される場所の明るさ（あるいはシステムのライトテーマ・ダークテーマ設定）によってもコンポーネントの見た目が変わることがあります。

![](/files/VpDARpfO4Joq9qVhefNP)

この 「プロダクト」と「明暗」の 2 つをまとめて Theme と定義しています。 3 種類の強さのボタンのように、同一テーマ内でのコンポーネントの種類分けを Variant と定義します。 Theme は visit-light や people-dark など、Variant はボタンであれば primary, secondary のようになります。

1 つの画面で利用される Theme は原則として 1 つですが、例外もあります。 たとえば Wantedly プロフィールページのグローバルヘッダには Variant が clear なテキストボタン・アイコンボタンがありますが、カバー画像の上に表示する関係で dark 系の Theme が使われます。

![](/files/SBNfijRNsB0yQx0gRWdE)

また、Surface の各種パラメタは Theme および Variant によって定められていますが、デザインによってはオーバーライドされることもあります。 たとえば Wantedly プロフィールのカバー画像下の CTA ボタンは Secondary と Primary の 2 つのボタンが並んでいます。 これらのボタンは本来 Elevation は異なる値が割り当てられていますが、「ページ上で 2 つ並んで浮いてるボタンが異なる高さに配置されている」というのは見た目上明らかに良くないため、Secondary の Elevation がオーバーライドされています。

Theme および Variant が定めるのパラメタには以下のようなものが存在します。

* Surface
  * Shape
  * Background Color
  * Elevation
  * Reaction 種別
* 特殊な Reaction の設定
  * Reaction のオーバーレイの色
  * Reaction ごとの Foreground Color
  * Elevation の影の色
* Foreground Color(文字色, アイコン色)

### その他の UI デザインシステム構成要素

#### TouchArea

コンポーネントのタッチ可能領域を広げることを目的として、実際の要素の外側に不可視のタッチエリアを設けることがあります。 ウォンテッドリーが提供するプロダクトは PC だけでなく画面の小さなスマートフォンでも多く利用されます。 小さな画面であまりに小さなコンポーネントを出していると、ユーザーはタップするのにも苦労してしまいます。 コンポーネントの間が詰まっていると誤タップしてしまうかもしれません。

Google や Apple のデザインガイドラインでは Interactive Element のタッチ可能領域の最低値を定義しています。これと似たような概念を原則として設けています。

* [Google の Material Design では 48px 四方](https://material.io/develop/web/supporting/touch-target)
* [Apple の Human Interface Guideline では 44px 四方](https://developer.apple.com/design/human-interface-guidelines/foundations/accessibility#buttons-and-controls)

個別のコンポーネントについてタッチエリアを定義するのではなく、原則として小さな要素についてはタッチエリアを設ける必要があると定義しています。特にモバイル向けのページ実装時に配慮されているか注意しましょう。

## UI デザインシステムを利用して UI コンポーネントを組み立てる

Surface の節では **「Wantedly のプロダクトの UI に登場するコンポーネントは原則この Surface から作られている」** という説明をしました。 これが実際どういうことなのか、例をいくつか見てみましょう。

たとえば Wantedly Visit で企業が見る候補者のプロフィール画面には、以下のような UI コンポーネントが存在します。 全体として薄いグレー（正確には透明度の高い黒）が下敷きにあり、その上にホバーやクリックで色が変わるアイテムが乗っています。

![](/files/WdILXwfcWxP38V62m3L8)

このコンポーネントは以下 2 つの Surface の組み合わせで作られています。

* 背景はグレーでリアクションがない、リスト全体の Surface
* 背景はないがリアクションで overlay が乗る、リストアイテムの Surface」の 2 つを組み合わせています。

（この場合だと背景もリストアイテムにつけることもできますが、どちらがいいかはケース・バイ・ケースでしょう）

![](/files/5X3qNLFd7no5czLw2mXV)

別の例として、以下の画像のようなクレジットカード情報入力フォームを考えます。 見た目上は 1 つのテキストフィールドコンポーネントに見えますが、実際は番号, 月/年, CVC の 4 つのフィールドが存在しています。

![](/files/IjbFwaQ1Tbb1zHptH6cS)

これも 2 種類の Surface の組み合わせであると考えます。

* テキストフィールドの見た目で、内側の要素の hover や focus で見た目が変わる Surface（HTML 的には div）
* 背景と Elevation は無いが文字色だけは残っているテキストフィールド（HTML 的には input）

このように、Wantedly のプロダクト上の UI コンポーネントの多くは Surface の組み合わせで説明できるようにデザインされています。 これを知っているか知らないかで、UI 実装のスピードや精度は大きく変わるので覚えておきましょう。

## その他覚えておくと良いこと・心がけ

最後に、「UI デザインシステムを利用する」「UI を実装する」など、デザイナーと協業することがあるソフトウェアエンジニアが覚えておくと良さそうなことを紹介します。

### UI デザインシステムの全てはオーバーライド可能である

UI デザインシステムを眺めていると、あらゆる値がカッチリ決まっているように見えるかもしれません。 実際、UI の一貫性を保つため定義通りの値を使うことがほとんどです。 しかし、たとえば「ユーザーに強く訴求したい」であったり「局所的な見た目の一貫性を優先したい」など、情報設計やビジュアル的な理由で、オーバーライドが発生することがあります。

### UI デザインシステムに定義されているもの・されていないもの

Wantedly の UI デザインシステムは Foundation, Surface など一見すると完成されたものに見えますが、実態としては一般化しきれていない例外などがまだまだ残っています。実装者・利用者はそのことを理解して、「各種パラメタは拡張に対して開いておく」「例外を無理に一般化せずに、ケース集にストックしておく」ことを意識しておくといいでしょう。

### 言葉は正しく使おう

[プロダクトデザイナーと上手に協働するための心得](https://docs.wantedly.dev/fields/apps/collaboration-with-designers)でも触れていますが、同じ言葉・同じ単語セットで会話をすることは円滑なコミュニケーションをする上で非常に重要です。 たとえば "Modal" という単語についてエンジニアとデザイナーでそれぞれ違うものを思い浮かべていた場合、話が噛み合わなくなります。（デザインとしての "モード" を表現するためのモーダルなのか、ダイアログ要素を指しているのか、など。）

また、関連して「勝手に単語を作らない」というのもちょっと意識するといいかもしれません。 ほとんどのケースでエンジニアはデザイナーよりも UI に関する知識が少ないため、エンジニアが「これと同じ概念かな？」と思ったら実は全然違っていた、みたいなことは起こりえます（Java と JavaScript は同じでしょ！って言われたら困りますよね？）。 UI コンポーネントの名前に自信がないときはデザイナーに確認して認識を合わせておくといいでしょう。 また、実装（React コンポーネント）の名前とデザイナーが使う名前を合わせておくと、よりコミュニケーションしやすくなってオススメです。

W3C が公開している [ARIA Authoring Practices Guide (APG)](https://www.w3.org/WAI/ARIA/apg/patterns/) を参考にするとよいかもしれません。

### デザイナーが何を考えているかを知ろう

「UI デザインは直感で見た目が良くなるように作られている！」なんてことはなく、そこには原理・原則のようなものが存在します。 「UI デザイナーがどういうロジックで UI デザインを作っているか」を多少なりとも理解できれば、それはそのまま実装時のモデリング（コンポーネント設計）にも反映でき、UI をより正しく実装する助けになるでしょう。 **正しいモデリングのもとに実装された UI はデザイナーの意図も反映されやすく、壊れにくい・拡張しやすいものになります**。 「ここ揃ってないんだけど？」みたいな指摘を受ける頻度もかなり変わってくるでしょう。

デザイナーがどういう原則のもとでデザインしているかを知るには、「[ノンデザイナーズ・デザインブック](https://www.amazon.co.jp/dp/4839955557)」を読んでみるといいでしょう。 デザインの「4 つの基本原則」などわかりやすく解説してくれています。 原則を知らない状態で UI を実装するということは、インデントを知らずにプログラミングをしてるみたいなものと言えるかもしれません。

[2019 年に実施されたノンデザイナー向けデザイン研修の資料 (internal)](https://wantedly.slack.com/archives/C91E7SCU8/p1653901056050939)もよくまとまってておすすめです。

### わからないことがあったら話をしよう

デザイナーの成果物を実装しているときに、そのデザインの意図がわからず実装が難しくなるケースがあると思います。 そういうときに「デザイナーと話して確認する」という選択肢を持つようにしましょう。 たとえば「ここだけマージンが他とちょっと違うんだけど、なんでだっけ？」みたいな、ときに「エンジニアがデザインに関する知識をつけてエスパーで解決する」みたいなことはできなくはないでしょう。 しかし、エンジニアはデザインの専門家ではありません。誤った推測から誤った設計になってしまうよりは、時間とって話すほうが結果手戻りが少なくなる可能性もあります。

もちろん、コミュニケーション回数が増えるとオーバーヘッドは大きくなっていきます。効率の良いコミュニケーション方法は考えてみるといいでしょう。

### フィードバックを恐れない

エンジニアが UI を実装していく中で違和感を持つことはたまにあるでしょう。 たとえば「実データを流し込んでみたらどうも変な感じする」や「実装した画面から情報を読み取りづらい気がする」など。 そういう違和感はデザイナーにどんどんフィードバックをしていくといいでしょう。 「[みんなではじめるデザイン批評](https://www.amazon.co.jp/dp/B01J2OEYLU)」は良いフィードバックのために意識すべきこと・目的・テクニックやコラボレーション方法について解説してくれています。 フィードバックする際には、感情でフィードバックするのではなくロジカルにフィードバックをすることが重要です。 この本では、それがどういうことか具体性を持って示されているので、学びになると思います。

デザイナーの業務は専門性の高いものなので、エンジニアからするとその成果物に対して何かフィードバックするのは難しい・あるいはおこがましいと思ってしまうかもしれません。 しかし、我々エンジニアがたまに勢い余ってバグを出すのと同じように、デザイナーの成果物も常に完璧というわけではありません。

ウォンテッドリーではエンジニア・デザイナーが一緒になってプロダクトを作っています。 なのでいいプロダクトを作るために、お互いにリスペクトを持ちつつフィードバックしあえるような環境にしていけるといいですね。

## 参考資料

* [ノンデザイナーズ・Wantedly デザインシステム完全理解ペーパー(元記事)](https://www.wantedly.com/companies/wantedly/post_articles/395772)
* [プロダクトデザイナーと上手に協働するための心得](https://docs.wantedly.dev/fields/apps/collaboration-with-designers)
* [Webアプリのデザインシステムライブラリ](https://docs.wantedly.dev/fields/apps/design-system)


# Web アプリのアーキテクチャ

本章では、生産性の高い状態で継続的に開発していくために、Web アプリのアーキテクチャについて議論した内容について紹介します。

## 技術スタックの現状

これまでのリニューアル[^1]、リノベ[^2]、リファインメント[^3]によりプロダクトのUIがアップデートされ、それに伴いフロントエンドの技術スタックもアップデートされてきました。 現在技術スタックは大きく4世代存在し、v1〜v4 と呼ばれています。

| version | 構成                                                                           |
| ------- | ---------------------------------------------------------------------------- |
| v1      | Haml / SCSS / AngularJS & jQuery & Backbone.js                               |
| v2      | TypeScript + React + Redux + redux-thunk / styled-components / webpack       |
| v3      | TypeScript + React + Redux + redux-thunk / styled-components / webpack / SSR |
| v4      | TypeScript + React(Next.js) + GraphQL / styled-components                    |

最新のバージョンは v4 です。現在でも v4 以前の古い世代の技術スタックで書かれたコードが存在します。

## 開発における基本方針

新規開発やページのリニューアルのタイミングでは可能な限り v4 で開発することを推奨します。 v4 アーキテクチャの各アプリケーション上で開発を行い、 GraphQL サーバーを経由して各マイクロサービスと通信を行うようにします。

### v4 アーキテクチャのアプリケーションについて

v4 のアーキテクチャのアプリケーションは以下の２つが存在します。

| アプリケーション                | 対象          |
| ----------------------- | ----------- |
| wantedly-frontend       | ユーザーサイト     |
| wantedly-admin-frontend | 企業向けユーザーサイト |

対象に記載がある通りそれぞれのアプリケーションがユーザーサイトと企業向けユーザーサイトに分かれています。 対象となる機能がユーザー向けであるか、企業向けであるかをもとに開発するべきアプリケーションを確認してください。

### GraphQLサーバーについて

WantedlyにはGraphQLサーバーが以下の2種類存在します

* wantedly-graphql-gateway
* visit-api-node

サーバーが2つ存在する経緯等については [Wantedly Visit で BFF GraphQL サーバーを辞めた理由](/fields/the-system/history-of-graphql) を参照ください。 新規ドメインの query/mutation は基本的に wantedly-graphql-gateway に用意されます。

## 実装における共通方針

### 技術方針の決定プロセス

新しい技術やライブラリを導入する際は以下の手順で進めます：

1. Frontend Chapter で議論を行う
2. 撤退可能な状態・最小規模で導入する
3. Architecture Decision Record (ADR) に記録する
4. 振り返りを行う
5. 振り返りをもとに判断を行い、必要であれば広く導入する

小さく試す限りにおいては上記の手順はSkipしても問題ありませんが、判断に困る場合は上記に従うことを推奨します。

### SSR

SSR が可能なアーキテクチャにおいては SSR は必ず行う前提で実装を行います。 一方で SSR の際、つまりサーバーサイドで多数の GraphQL の呼び出し等をすると TTFB (Time to First Byte) が遅くなる原因になります。

![TTFB image](/files/KsxpnDsMf4IIcPuKM3Cv)

そのため、何をサーバーサイドで同期的に取得し、何をクライアントサイドで非同期的に取得するかを個々のページで考慮する必要があります。 例えばファーストビューで表示されるコンテンツ SEO に必要な meta 情報はサーバーサイドで同期的に取得する必要があります。 一方でパーソナライズされたコンテンツ、例えばユーザーへおすすめのコンテンツなどはクライアントサイドで非同期的に取得を行っても問題ありません。

![TTFB image](/files/ucNhfP0iPrUBcb9Z6LxP)

画面の性質を考慮した上で適切な設計を行うようにしてください。

### 状態管理

Web アプリの状態管理は以下の3種類に分けることができます。

| No. | 種別名          | 概要                                |
| --- | ------------ | --------------------------------- |
| 1   | サーバーデータキャッシュ | サーバーから取得したデータをクライアント側でキャッシュしているもの |
| 2   | グローバルな状態     | 複数画面/コンポーネントで保持するUIの状態            |
| 3   | ローカルな状態      | 単一のコンポーネントが保持するUIの状態              |

これらの状態管理をそれぞれの技術スタックで以下のように状態管理を行ないます。

**1. サーバーデータキャッシュ**

| 技術スタック | 何を使っているか             | 補足                        |
| ------ | -------------------- | ------------------------- |
| v1     | -                    | フロントエンドでのデータキャッシュは行なっていない |
| v2, v3 | Redux Store          | 一部 React Query を利用している    |
| v4     | Apollo InMemoryCache | -                         |

**2. グローバルな状態**

| 技術スタック | 何を使っているか    | 補足                                                        |
| ------ | ----------- | --------------------------------------------------------- |
| v1     | -           | 単一の状態管理を持たない                                              |
| v2, v3 | Redux Store | -                                                         |
| v4     | -           | 必要に応じた Context API を利用した状態管理はあるが Redux のように中央集権の状態管理は持たない |

**3. ローカルな状態**

| 技術スタック | 何を使っているか               | 補足                                       |
| ------ | ---------------------- | ---------------------------------------- |
| v1     | -                      | 単一の状態管理を持たない                             |
| v2, v3 | Redux Store / useState | Redux Store が存在する場合は Redux Store を利用している |
| v4     | useState               | -                                        |

前述のとおり v4 では Redux のような中央集権的な状態管理の仕組みは存在しません。 これはグローバルな状態管理のために Redux, Jotai, Zustand などのライブラリを使用しない、という判断ではありません。 v4 において複雑な状態管理が必要になった際には新規にライブラリを導入するという判断は可能です。 技術方針の決定プロセスの手順に従い導入を進めるようにしてください。

## v4 へのマイグレーション・移行

wantedly/wantedly の v1 スタックのページについてはリノベ・リニューアルのタイミングで積極的に v4 への載せ替えを推奨とします。 移行については以下のいずれかの方針で進めることを推奨します。

* v1 から v4 へ完全移行（GraphQL API への移行も含む）
* v1 から v4 へフロントエンドを移行（GraphQL APIは対応せず、既存のエンドポイントを利用する）
* v1 から v2, v3 へ、その後 v4 へ段階的に移行する

どのような方針で進めるべきか迷った場合はまずは Frontend Chapter で相談をしてみてください。

#### 話を聞きに行きたい

* Slack: [#frontend\_chapter](https://wantedly.slack.com/archives/CA46K091P)

#### もっと知りたい

* [Wantedly Visit のウェブフロントエンドの構成の歴史 (internal)](https://github.com/wantedly/wantedly/blob/master/doc/frontend_history.md)
* [Wantedly のこれからの Frontend Architecture を考える (internal)](https://github.com/wantedly/dev/issues/444)
* [Frontend Architecture を考える 2021春 (internal)](https://docs.google.com/document/d/1U-Rwad-R76RgQ0OrDvhT0BBc_pcNOKZiH3h0Eb25Pe8)
* [Web Frontend Directory structure 2021 秋 (internal)](https://docs.google.com/document/d/1BqczG15wWQxtXszo2T6FU7JwbnIv3baG9PEMdRvEGO8)

[^1]: フロントエンド/バックエンド含めてアーキテクチャ/機能/UIを刷新する。（e.g. 2019-2020年頃の募集作成画面、スカウト画面のリニューアルプロジェクト）

[^2]: バックエンドは変更せず、UIのみを大幅に変更する。（e.g. 2020-2021年頃の採用管理画面 UI 刷新プロジェクト）

[^3]: UI は維持したまま、フロントエンド/バックエンドの刷新を行う。（e.g. 募集一覧画面、募集詳細画面の v 1→ v4 移行プロジェクト）


# プロダクトデザイナーと上手に協働するための心得

## はじめに

ウォンテッドリーのエンジニアには、課題発見からリリース、そして結果分析までの一連の改善ステップを、オーナーシップを持って進めることが期待されます。その中のフロントエンドの実装において、プロダクトデザイナーと上手くコミュニケーションをとって協働することは、改善スピードを上げるためにも、またプロダクトの品質を上げるためにも重要なスキルになります。

本章では、ウォンテッドリーのエンジニアとして、プロダクトデザイナーとのコミュニケーションのあり方や、プロダクトをスムーズに開発するために気をつけることを説明します。前半では、一般的にデザイナーと協働するために心得ておくと良いことを、後半では、スムーズに仕事を進めるための心得について、リリースまでの全体像を説明した後、各ステップごとに気をつけるべきことを説明します。

## 上手く協働するための心得

デザイナーと上手く協働するテクニックは色々とあると思います。また、一緒に働く人によって、上手く協働する方法はそれぞれ異なることもあるでしょう。一方で、普遍的に大切なこともあります。中でも、 [Communication Between Designers and Engineers, WWDC2017](https://developer.apple.com/videos/play/wwdc2017/809) で紹介されている項目が良かったので紹介します。

この発表では、iOS アプリを実装する上で、デザイナーと協働するために次の 4 つが大切であると紹介しています。

* 共通言語で話す
* 唯一の情報源を持つ
* 幅広い観点を持つ
* 語るよりも見せる

この項目に沿って、ウォンテッドリーではどのように気をつけるべきかを説明します。

### 共通言語で話す

UI を説明する言葉がバラバラであったり、エンジニアとデザイナーとで異なる用語を使っていると、デザインに対して認識の齟齬が発生してしまい、デザイナーが意図していないデザインを組んでしまうことがあります。

デザイナーとエンジニアが共通の言語でデザインについて会話することは、コンテキストの共有を促し、よりスムーズなコミュニケーションを促進します。

例えば、ボタンについて考えてみます。もう少しボタンを強調したいと思った時、デザイナーにどう説明しますか？

この場合、ウォンテッドリーでは、Elevation を用います。例えば、「このボタンの Elevation を上げるとどうですか？」というように話します。「このボタンの影を `box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1), 0px 0px 0px 1px rgba(0, 0, 0, 0.02)` にするとどうでしょう？」というより、よっぽど楽に議論ができると思います。

一般的に、デザインシステムは、デザイナーとエンジニアの両方が、アプリケーションの構築方法を一致させるために話すことのできる言語として機能します。ウォンテッドリーでも、デザインシステムを共通の言語として、普段からコミュニケーションを取っています。

さらに、コードベースでも同じコンテキストで UI を記述できることを目的に、デザインシステムライブラリを作っています。詳しくは [Web フロントエンドアプリのデザインシステムライブラリ](/fields/apps/design-system) を確認してください。

### 唯一の情報源を持つ

デザインの参照元が複数あると、どれが信頼できるデザインか分からなくなることがあります。参考にするデザインは必ず一つの情報源にしておきましょう。

ウォンテッドリーでは、新しいデザインに関しては Figma を使って一元管理しています。古いデザインに関してはデザインの情報源がどこなのか、最初にデザイナーに確認しておくと良いでしょう。

### 幅広い観点を持つ

デザインだけでなく、デザインの外側にあるものについても考えることが重要です。具体的には、ユースケース、リソースキャパシティとの兼ね合い、アクセシビリティの観点も考えることが重要です。

例えば、Web やモバイルアプリの標準から外れてカスタム UI を作るのは、かなりコストの高い作業であると認識するべきです。[Communication Between Designers and Engineers, WWDC2017](https://developer.apple.com/videos/play/wwdc2017/809) では具体的な例として、カスタムされた UI ではなく、iOS 標準のテーブルを使うべきだと主張しています。大抵の場合は、デフォルトであったりライブラリが提供しているフレームワークを使う方がアクセシブルであり、実装工数もかかりません。

ウォンテッドリーの場合、デザインはデザインシステムに則って実装されるので問題はないことが多いです。ただし、フロントエンド実装において難しいポイントは抑えておくと良いでしょう。Web アプリで例を挙げると、「...続きを読む」のようなデザインを実装する場合です。truncate + inline block の組み合わせは、デフォルトの CSS では対応できず、複雑な制御を行う必要があるため実装コストがかかります。

実装コストがかかる場合、デザインにフィードバックする段階で指摘できると良いです。頑張ってデザインを実装するのではなく、デザイナーに正直に実装が難しいことを伝えて代替案を検討することも考慮に入れましょう。

また、さまざまな状態についても熟慮するべきです。デザインはあくまでも状態の一部を切り取ったものにすぎません。その画面が取りうる状態によってデザインが変わります。提示されているデザインで全ての状態がカバーできるか、状態が変化した時の接続に問題がないかなどをあらかじめ確認しておきましょう。

### 語るよりも見せる

実際にデザインや実装を見ながら話す時間を設けましょう。デザインを見ながら、または見せながら議論することで、デザイナーからは実装が意図と合っていないという不満を、エンジニアからは仕様が詳細でなかったために心の隙間が埋まらないという不満を解消できます。

見せ方ですが、なるべく静的な画面のキャプチャーではなく、動くものを実際に同期的に見せましょう。そのために、フィードバックする時間や、デザインチェックする時間など、デザイナーと議論する時間を同期的に作ります。

また、全て完成してから見せるのではなく、見せられるものができたら早めに見せましょう。特に大きな施策を動かしている時は、大きな手戻りを防ぐためにも気をつける必要があります。UX は、一発目のデザインで完成しているのではなく、実装していく中で洗練されるものです。

次の節からは、実際に行っているステップを紹介し、その中で気をつけるべきことをより具体的に説明していきます。

## スムーズに仕事を進めるための心得

ウォンテッドリーでは、プロダクトデザイナーと一緒になって、ユーザーの課題解決のために施策やプロジェクトを進めます。具体的には次に示すようなステップで仕事を進めます。

<img src="/files/lsga7eHbpEbUrDE0zbYe" alt="" width="800">

デザインをフィードバックする時間と、実装をチェックするデザインチェックの時間は、なるべく同期的に設けるようにしましょう。フィードバックやデザインチェックは、状況によっては手戻りが発生することもあります。特にデザインチェックで手戻りが発生した場合、修正したら直接リリースして大丈夫だろうと思わず、リリース前には必ずデザインチェックをお願いしましょう。

次節からは、各ステップで気をつけることを説明していきます。

### デザイナーを巻き込む

#### 課題を言語化して伝える

デザイナーにデザインをお願いするときには「なぜやるのか？」を伝えることが最も大切です。次のことを伝えましょう。

* 課題の仮説
* 解決方法の案

課題に対しての解決方法は一つだけではなく、デザイナー視点で見たときにより良い解決方法に気づくこともあります。そのため、「ユーザーにとって何が課題であるか？」を具体的に伝えることが重要です。

課題の仮説では、事実も含めて書くことでより仮説の信憑性が増すため、一緒に伝えると良いでしょう。課題について深く認識することがより良いユーザー体験につながり、また重要な課題であると認識できることが、デザイナーのモチベーションにもつながります。

解決方法の案では、その解決方法が本当に課題の解決になっていることを確認しましょう。また、解決方法を言葉だけで 100%理解してもらうのは思ったより難しいので、プロトタイピングすることがお勧めです。プロトタイピングには Miro などのボードを使うことが多いです。

課題の見つけ方については、[プロダクトの課題発見及び解決](/fields/dev-process/discovering-and-solving-service-issues) を確認してください。

また、デザイナーが後から優先度判断がしやすいように、課題仮説を見やすい場所に記述したり、実施する施策の一覧を参照できるようにしておきましょう。

#### プロジェクトをスムーズに進行させるための Tips

プロジェクトの存在理由や目標の目線を合わせておくために、Kick-off などの早い段階で、デザイナーを巻き込むようにしましょう。

また、次の事柄をあらかじめデザイナー側と握っておくと以降のステップがスムーズに進みやすいです。

* スケジュールを握る
  * いつまでにデザインが必要か
  * いつデザインチェックをするか
* コミュニケーションの方法を握る
  * 同期的にコミュニケーションをとって進めるべきか、非同期コミュニケーション（Slack 上のやりとりや GitHub の Issue/PR など）で十分か。
    * なるべく同期的にやるべき。
* 意思決定を握る
  * どこまでエンジニア側で対応して良いか
    * 文言や考慮もれのエッジケースがあった場合、必ずデザイナーとチェックすべきかどうか

### デザインのフィードバック

クリティカルシンキングでフィードバックを行いましょう。自分の感覚でデザインの良し悪しをフィードバックするのではなく、提示されたデザインが課題を解決しているか、ユーザーにとって悪い体験になっていないかなどの観点から、ロジカルにフィードバックすることが大切です。

どのようにフィードバックすると良いかについては、 [みんなではじめるデザイン批評―目的達成のためのコラボレーション&コミュニケーション改善ガイド](https://www.amazon.co.jp/dp/B01J2OEYLU) を一読することをお勧めします。

また、「幅広い観点を持つ」の節で説明したことで、問題がないかこのタイミングで確認しましょう。抜けていた仕様がある場合などは、そのデザインもこのフィードバックのタイミングで議論します。

### 実装

デザイナーの意図通りにスタイルを実装していくことが大切です。そのためにも、まず、デザインシステムに則ってデザインを組みます。なるべくデザインシステムのライブラリを用いるようにしましょう。また Figma などの成果物をしっかりと見ることも大切です。例えば、そのコンポーネントが margin を持つのか、それともコンポーネント自体が高さを持っているかなど、成果物の通りになるように、デザインを組みましょう。

スタイルの崩れは伝播します。一つコンポーネントのスタイルが崩れてしまうと、それが全体に波及します。無駄に margin を使っていないか、必要のない場所で important や z-index を使っていないかなど、CSS の記述にも気をつけましょう。余裕があれば [Learn CSS](https://web.dev/learn/css/) などのサイトで体系的に CSS を学んでおくとより良いです。

### 実装のデザインチェック

大きな手戻りを防ぐためにも、なるべく早めにデザイナーとデザインチェックをする時間を設定しましょう。

また、前節で書いたように、全体を通したユーザー体験に問題がないかをチェックするためにも、静的なキャプチャーではなく、実際に動いているものをデザイナーにチェックしてもらいましょう。実際にデザイナーが触って確認できるように、キャプチャや Gif の他にも、QA 環境の URL を用意しておくと良いです。

## まとめ

前半では、デザイナーと上手く協働するための心得について 4 つ紹介しました。大切なこととして、共通の言語を用いてコンテキストを共有すること、Figma などのツールを用いること、デザイン以外の側面にも気を向けること、デザインを実際に議論する場を適切に設けることがあります。また、後半では、スムーズに仕事を進めるための心得について、実際に 5 つのステップでデザイナーと関わりながら仕事をすることと、それぞれのステップで気をつけるべきことを説明しました。

デザイナーと上手く協働することで、より早く、より品質の高いプロダクトをユーザーに届けることができるようになります。 一方で、現実では、リモート環境であったり、リソースが限られている場合であったり、外部のデザイナーと協力する場合であったり、様々なケースがあります。その場合も臨機応変に対応しながら、デザイナーと協力して、より良いプロダクトを一緒に作っていきましょう。


# Web アプリのデザインシステムライブラリ

## TL;DR

* Wantedly の UI デザインシステムは「WantedlyのUIをデザインする上での**共通の考え方とツール＆アセット**」であり**エンジニアとデザイナが効率よくコミュニケーションするための共通言語**となる
* デザインシステムを (Web) Frontend に持ち込む際は、単なるコンポーネントカタログではなく、システムが定義するものと同じレベルの抽象を持つライブラリ・フレームワークとして実装することで、より有効性を発揮する

## UI デザインシステムについて

ウォンテッドリーにおけるデザインシステムは、「プロダクト・デバイスをまたいでも・誰がデザインしても体験やブランドとしての一貫性を保つ」「デザインの生産性を向上させ、デザイナ - エンジニア 間コミュニケーションを改善することで、ユーザーに価値を届ける速度を向上させる」といった目的のために作られたものです。

より詳しくは、[デザインシステムが加速させるプロダクト開発 / Design System and Scalable Product Development](https://speakerdeck.com/kawasy/design-system-and-scalable-product-development)を参照してください。

次の2つの画像にあるように、Wantedly のプロダクトなどをデザインするときに利用するフォントなどのスタイルと、ボタンやテキストフィールドなどの UI コンポーネントなどが定義されています。

![](/files/JrD0nX7TI3HOO7DjXAyG)

![](/files/2fB7SU5OutATBm6Wt1G3)

## Wantedly の UI デザインシステムとは何か・何でないか

ウォンテッドリーが作っている「UI デザインシステム」とは何か、内部のドキュメントでは次の一文で表現されています（強調は筆者による）。

> WantedlyのUIをデザインする上での**共通の考え方とツール＆アセット**

この「共通の考え方とツール＆アセット」とは何か、いくつか具体例を見ていきます。

たとえば「Button」を表すコンポーネントですが、Figma 上では以下のような決められた設定を持ちます。

![](/files/OON44HwMA3Z5I6B4LFhd)

また、上記の画像では Button の見た目よりも少し外側に線が出ていますが、実際にその空白まで含んで Button であると定義されています。これは TouchArea と呼ばれ、そのコンポーネントが持つべき最低限の余白を表しています。これは Button や TextField などの Interactive なコンポーネントに適用されており、余白自身もコンポーネントの一部になります。

> TouchArea によりコンポーネント間の余白がいい感じになるの図。この領域を含めて Apple や Google の Interface Guideline を満たすように設計されている。

ここまで例に上げたとおり、Wantedly の UI デザインシステムは「コンポーネントカタログ」ではなく、コンポーネントの構成要素・原則からなる「**共通の考え方とツール＆アセット**」であるというのがなんとなくわかってもらえたでしょうか。

## なぜ UI デザインシステムを作るのか

ウォンテッドリーでは何のために UI デザインシステムというものを作っているのか、社内のドキュメントでは以下のような目的が挙げられています。

* ブランド表現 - Wantedlyとしての見た目と振る舞い の一貫性を保つ
* ベーシックなユーザビリティの担保
* デザインアウトプットの効率化
  * 細かい造形で悩まず、プロダクトとして大切な体験にフォーカスできるように
  * 複数のプロダクトをまたいでも、共通の考え方で対応できるように
* エンジニアとのフロントエンド開発、コミュニケーション、メンテナンスの効率化

ここでは「エンジニア」と明記されている「エンジニアとのフロントエンド開発、コミュニケーション、メンテナンスの効率化」について、なぜそうなるのかをもうちょっと掘り下げます。前節での「デザインをする上での共通の考え方とツール＆アセット」というのを前提として考えると、以下のような理由があると考えています。

* （UI）デザインシステムが、**エンジニアとデザイナが効率よくコミュニケーションするための共通言語**となる
* UI デザインシステムにより、**コンポーネントの作り方に一貫したルール**が生まれる
  * このルールを知り、使いこなす（使いこなせるツールが存在する）ことで、誰でも「Wantedly らしい UI コンポーネント」が作れる

「デザイナがどういう言語を用いて、どのようなルールの上でデザインをしているか」をエンジニアが知ることで、コミュニケーションが円滑になり、背景・ロジックを知った上で実装ができるようになる。それにより「エンジニアとのフロントエンド開発、コミュニケーション、メンテナンスの効率化」が達成できるのではないでしょうか。

余談ですが「デザイナがどういう言語を用いて、どのようなルールの上でデザインをしているか」というのは、Web エンジニアが Ruby on Rails や Next.js といったフレームワークを利用するのと似たような構造に思えます。ウォンテッドリーのデザイナは**デザインシステムというフレームワークに乗ることで、細かいことを気にせずプロダクトの価値を生み出すことに集中できる**と解釈するとわかりやすそうです。

## React 実装の設計

前節で（UI）デザインシステムは「**エンジニアとデザイナが効率よくコミュニケーションするための共通言語**」であり「**コンポーネントの作り方に一貫したルール**」をもたらすものである、という解釈をしました。また、「**UI デザインシステムというフレームワーク**」とも表現しました。これを実現するために、エンジニア向けの実装はどのようにあるべきでしょうか。

「共通言語」であり「一貫したルール」であるために、エンジニアとデザイナはデザインシステムに関して**同じレベルの抽象**をもってコミュニケーションしていく必要があるはずです。ライブラリとして提供する実装に関しても、基本的にはデザイナが扱っている抽象・フレームワークと同じレベルのものを提供していく必要があるでしょう。

「同じレベルの抽象」というとかなり抽象的な表現ですが、誤解を恐れずに言い換えると「**デザイナと同じロジックでコンポーネントを作れること**」ということでしょうか。わかりやすいところでいうと「デザイナがコンポーネントを作るときに指定するパラメーターが、そのまま実装されている」などです。もっと深堀りすると「ここのMessage は 見出し2 で...」みたいなものが、正しく React コンポーネントの実装およびインターフェースとなっている必要があります。

![](/files/ynf6ZE8Ti5NzvVo9rc24)

```typescript
const Message = styled(createText('h2', { textStyle: "headline2" }))``
```

ここまでで満たすべき要件を確認したので、設計に移っていきます。前提として、TypeScript + React でシステムを実装していきます。これはウォンテッドリーにおける Web Frontend の主要な技術選択にあわせています。内部では [styled-components](https://styled-components.com/) を利用していますが、これは peerDependencies を除きインターフェースとして露出することは無いようにしています。

### UI デザインシステム構成要素について

これは React 実装に限らない UI デザインシステムの定義上の話です。UI デザインシステムは次の3\~段階の要素から構成されています。

* **Lv.0 Foundation**
  * 一貫した Corporate Branding のためのシステム
  * UI に限らない Graphic Standards: 色, フォント, アイコン など
  * UI で守るべき基本原則: Layout unit, Shape, Elevation, Reaction, Typographic scale
* **Lv.1 Basic Components**
  * (Designer|Developer) Productivity を向上させるためのシステム
  * ボタンやテキストフィールドなどの基礎コンポーネント群
* Lv.2 ここまでの要素の組み合わせからなる UI コンポーネント群
* Lv.3 プロダクトの UX を向上させるための機能コンポーネント群
* ...

Lv.2 以降に関してはそこまでのコンポー ネントの組み合わせが主になります。よって、Lv.0 Foundation および Lv.1 Basic components を React の上でどう表現するかがポイントになってきます（Lv.2以降についてはまたの機会があれば紹介します）。

### Lv.0 Foundation

Lv.0 Foundation には前述したとおり、色・フォント・アイコン・Elevation などのデザインパラメタが定義されています。また、[Elevation](https://material.io/design/environment/elevation.html) や Reactionなど UI コンポーネントの振る舞いの原則も含まれます（Reaction: [Material Design における State](https://material.io/design/interaction/states.html#usage) のような、Hover や Focus など、ユーザーのアクションによってコンポーネントに起こる反応のパターン）。

さて、この Foundation はどのように定義されていると良いでしょうか？ css の Syntax Sugar として定義されてほしい場合と、Component のパラメーターとして定義されていてほしい場合がありそうです。

```js
// css の Syntax Sugar として定義
const HeadText = styled(createText('span', { textStyle: "headline1" }))`
  color: ${colors.blackAlpha800};
  white-space: pre-wrap;
`
```

### Lv.1 Basic components

Foundation の実装は Basic componentsとして実装されています

```js
const Button = styled(createButton('button', {variant: "button-primary", size: "medium"}))``
```

コンポーネントの実装は、ui-react-2022によって提供され、必要に応じてstyled-componentsでcssを上書きすることになります。

## まとめ & Future work

ここまでで、「Wantedly における UI デザインシステムとはなにか」「デザインシステムの React 実装の設計」について紹介しました。この記事で紹介したデザインシステムの実装は、Wanteldy の最近のプロジェクトで活用されています。

一方で、デザインシステムについては、ここで紹介した React 実装以外にもやるべきこと・やると良さそうなことが無数に残っています。いくつか例を上げると下のようなものがあります。

* Android および iOS 向けの実装
* デザイナ - エンジニアのさらなるコミュニケーション改善のためのエンジニアリング
* エンジニア実装（React, Android, iOS, ...）とデザイナ実装（Sketch or Figma）のインテグレーション

#### 話を聞きに行きたい

* Slack: [#frontend\_chapter](https://wantedly.slack.com/archives/CA46K091P), [#design\_system](https://wantedly.slack.com/archives/CAS1153PD)
* GitHub: `@wantedly/design-system-implementer`

#### もっと知りたい

* [React でデザインシステムを正しく実装する - コンポーネントカタログを超えて | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/302873)
* [UIデザインに必要な『一貫性』へのアプローチ - 管理画面のアップデートに際して | Wantedly Design](https://www.wantedly.com/companies/wantedly/post_articles/306400)
* [ノンデザイナーズ・Wantedly デザインシステム完全理解ペーパー](https://www.wantedly.com/companies/wantedly/post_articles/395772)


# Web アプリ共通ライブラリ "React Shared Component" の紹介

## React Shared Components とは何か

React Shared Components は以下の2つのパッケージ提供します

* **shared components**: v1\~v3 も含めて複数箇所で共有する Frontend v4 の技術をベースとしたコンポーネント
* **frontendx**: Frontend v4 の各リポジトリ間で共有される React Hooks 、ユーティリティ [servicex](/fields/the-system/servicex) の思想を引き継いでいる

## 背景

### 前提

[Webアプリのアーキテクチャ](/fields/apps/frontend-architecture)に記載があるように、 Wantedly のフロントエンドのアーキテクチャは技術スタックをアップデートしてきました。 そのため現在はフロントエンドのコードは複数のリポジトリに分かれています。

### shared components

複数リポジトリにフロントエンドのコードがある一方で

* ヘッダー
* サイドバー
* フッター

といったナビゲーションなどのコンポーネントはリポジトリ間で共通の UI が存在しています。 例： 以下の画面はそれぞれ異なるリポジトリで実装されており、青色で囲われた部分は共通の UI です。

![例](/files/-MhWAMwej_cjDsoN4Jta)

これらコンポーネントを複数リポジトリで個別で管理すると

* デザインの一貫性が担保できなくなる（特定のリポジトリのみが修正されている状態になる等が発生する）
* 個別で実装することでの機能差分が生まれる（特例のリポジトリのみ独自の機能が追加されている状態になる）

など、メンテナンス性・安定性を損ねることになります。 そのためこれらコンポーネントをパッケージ化するために `shared components` が誕生しました。

### frontendx

Frontend v4 のリポジトリでは、コンポーネントだけではなく、

* Feature Flagsの管理
* A/Bテストの状態管理

といった React hooks /ユーティリティについても個別に実装されるという事象が発生していました。 これらもコンポーネントと同様に個別で実装されることでメンテナンス性・安定性を損ねていると言う問題があり、 `frontendx` が誕生しました。 またこれら共通化を行うコードは共通のコンポーネントである `shared components` からも利用されうるというところから、 `React Shared Components` にて管理がされています。

## このパッケージがやること/やらないこと

### shared components

#### やること

* Frontend v4 におけるコンポーネントの共有
* Frontend v1\~v3 から v4 移行を前提とした全アーキテクチャでのコンポーネント共有

#### やらないこと

* 単一リポジトリでしか使用しないコンポーネントの提供

### frontendx

#### やること

* `shared components` を含む Frontend v4 で利用されるコードの共有

#### やらないこと

* 単一リポジトリでしか使用しないコードの提供
* Frontend v1\~v3 での直接的な利用
  * frontendxは v4 でのコードを共有することを目的としている
  * `shared components` は v4 でのコンポーネントを v1\~v3 で利用できるようにする
    * そのため `frontendx` は `shared components` をサポートし、 v1\~v3 にて利用することで間接的に利用されることを許容する

## React Shared Components と UI との違い

React Shared Components はここまで記載した通り、 Wantedly における**機能を提供**します。 一方 UI は、デザインシステムで規定されている**全プロダクト共通の UI システムやコンポーネント**を提供します。 前者は**機能**を、後者は **UI システムやコンポーネント**を提供すると言う責務の違いがあります。

## 提供されるパッケージ

現在提供されているパッケージは以下のREADMEを参照してください [react-shared-components README(internal)](https://github.com/wantedly/react-shared-components)

原則として1つの共通コンポーネントを1つのパッケージとして提供しています。 e.g.) ヘッダー/フッター

使わないコンポーネントを install しないため、単位でパッケージを提供しています。 また上記の理由から React Shared Components は monorepo で、管理されています。

#### 話を聞きに行きたい

* Slack: [#frontend\_chapter](https://wantedly.slack.com/archives/CA46K091P)

#### もっと知りたい

* [Lerna による monorepo の管理について (internal)](https://dev-docs.wantedly.com/javascript/lerna)
* [react-shared-components デザインドキュメント (internal)](https://github.com/wantedly/react-shared-components/blob/master/docs/design.md)


# モバイルネイティブアプリについて

この章では、Wantedly のモバイルアプリのアーキテクチャをまとめます。 まず Wantedly が提供するアプリの全体像を示したうえで、以降はもっとも開発が活発でアーキテクチャの中心となっている Wantedly アプリに絞って解説します。

この文書は継続的にメンテナンスされ、モバイルエンジニアのオンボーディングと日常的な開発作業の指針となることを目的としています。

## Why

モバイルアプリにおいて、アーキテクチャに世代が生まれるのは必然です。 既存のアーキテクチャの課題を解決するために新しい世代のアーキテクチャが生まれ、また、パラダイムシフトが起きるとアーキテクチャが変わっていきます。

アーキテクチャは、世代が新しくなるたびに改善していきます。 しかし、古くなったアーキテクチャをすべて一度にリニューアルするのは、膨大なコストが掛かるため非現実的であり、徐々に古いものを新しくしていくのが現実的です。 そのため、常に古い世代のアーキテクチャと向き合う必要があります。

この章では、古いアーキテクチャについては軽く、現在のアーキテクチャについてはより詳細にまとめます。

## Wantedly のモバイルアプリ

現在、モバイルアプリとして Wantedly・Wantedly Intern・People・Perk を公開しています。

### Wantedly

Wantedly アプリは、ユーザーと会社が気軽にマッチングできるアプリです。

* 2014 年から提供しています。
* 2018 年の iOS リニューアル時に React Native が導入されました。
* 2021 年に React Native は取り除かれ、KMP（Kotlin Multiplatform）が導入されました。

Wantedly アプリは iOS / Android の 2 プラットフォームで提供しており、ビジネスロジックの多くを KMP で共有しています。 現在、これらは単一のモノレポ [wantedly/visit-app](https://github.com/wantedly/visit-app) に統合されており、次の構成になっています。

* `iosApp/` — iOS アプリ（Swift）
* `androidApp/` — Android アプリ（Kotlin）
* `shared/` — iOS / Android で共有する KMP コード

### Wantedly Intern

Wantedly Intern は、学生の流入を目的としたアプリで、Wantedly アプリと同じリポジトリ・同じコードでできています。 そのため、Wantedly アプリと全く同じアーキテクチャで同じ機能を持ちます。

### People

People は、名刺や人脈を管理するアプリです。 2016 年から提供しています。 つながりの情報をローカルに保持し、各 OS の連絡先に同期する機能があります。このために一部に Realm が使われています。 名刺撮影機能のために OpenCV が使われています。 両 OS どちらも MVVM アーキテクチャを採用しています。

### Perk

Perk は、企業の従業員が提携先の優待などを利用できる福利厚生アプリです。 Flutter 製で、iOS / Android を単一のコードベースで提供しています。 アプリの大部分は WebView で構成され、認証などはネイティブ側が担っています。 アーキテクチャは domains / infrastructure / usecases / features の 4 層からなるクリーンアーキテクチャを採用し、状態管理と DI には `provider` パッケージを使っています。

## アーキテクチャの遷移

以降は、もっとも開発が活発な Wantedly アプリを対象に、そのアーキテクチャの移り変わりを示します。細かな点は省いています。

| OS      | 世代 | 言語     | アーキテクチャパターン                  | UI実装                 | 備考                                 |
| ------- | -- | ------ | ---------------------------- | -------------------- | ---------------------------------- |
| iOS     | 1  | Swift  | Flux(ReactorKit)             | UIKit(No Storyboard) | 2018年リニューアル                        |
| iOS     | 2  | Swift  | Flux(KMP Reactor)            | UIKit(No Storyboard) | KMP導入(2021年)                       |
| iOS     | 3  | Swift  | Flux(KMP Reactor)            | SwiftUI              | SwiftUI導入(2022年)。パッケージ管理は SPM に一本化 |
| Android | 1  | Java   | MVC                          | XML                  | 段階的リニューアル開始                        |
| Android | 2  | Kotlin | Flux(ReactorKitライクViewModel) | XML                  | UdfViewModel採用                     |
| Android | 3  | Kotlin | Flux(KMP Reactor)            | XML                  | KMP Reactor導入                      |
| Android | 4  | Kotlin | Flux(KMP Reactor)            | Jetpack Compose      | Compose移行中                         |

## サブページ

Wantedly アプリの現在のアーキテクチャを、共有ロジックと各プラットフォームに分けて解説します。 詳細は以下のサブページを参照してください。

* [iOS](https://docs.wantedly.dev/fields/apps/mobile-ios) — iOS アプリのアーキテクチャ。依存性注入、Coordinator パターンなど。
* [Android](https://docs.wantedly.dev/fields/apps/mobile-android) — Android アプリのアーキテクチャ。依存性注入、ナビゲーションなど。
* [Kotlin Multiplatform (KMP)](https://docs.wantedly.dev/fields/apps/mobile-kmp) — iOS / Android で共有するビジネスロジック層。Reactor / SQLDelight / GraphQL。

## 参考資料

### 技術ブログ・記事

* [React Nativeをやめる話とKotlin Multiplatform](https://www.wantedly.com/companies/wantedly/post_articles/282562)
* [Wantedly VisitにおけるKotlin Multiplatformの導入と実装](https://www.wantedly.com/companies/wantedly/post_articles/300999)

### 質問・相談

* Slack: [#mobile\_chapter](https://wantedly.slack.com/archives/C014VN3KKB9)


# iOS

想定読者:

* iOS アプリの開発を担当するモバイルエンジニア
* Android を主とし、iOS 側の構成を把握したいモバイルエンジニア

本章では Wantedly の iOS アプリのアーキテクチャを扱います。 ここでは iOS ネイティブに閉じた話を対象とし、個別の実装やコードの詳細には踏み込まず、全体像を素早くつかめることを目的とします。

モバイルアプリの開発では、新しい技術の登場やプロダクトの課題の変化にあわせて、アーキテクチャは必然的に世代交代していきます。 全面的に作り直して一度きれいにしても、その効果は長続きせず、いずれまた段階的な改善を迫られます。

だからこそ、個々のアーキテクチャや実装そのものよりも、その選択の背後にある思想・戦略を理解することが重要です。 本章では Wantedly の iOS アプリの構成を具体的に示しながら、なぜその形を選んでいるのかという考え方を中心にまとめます。

## 概要 — 設計思想

Wantedly の iOS アプリは次の原則に基づいて設計されています。

* 段階的リニューアル: 全面刷新は行わず、画面・機能単位で少しずつ置き換える
* 単方向データフロー: 状態の流れを一方向に保ち、挙動を予測可能にする
* Single Source of Truth: 多くのデータは端末内データベースを情報源とし、画面はその変更を購読する
* モジュール分割: 役割と機能でモジュールを分割し、規模の拡大に耐える
* ロジックの共有: ビジネスロジックを Kotlin Multiplatform (KMP) で Android と共通化する
* 移行を前提とした運用: 新規実装は新しい方式に寄せ、古い方式の新規追加を避ける

iOS は 2018 年に全面リニューアルを経験しています。 このときはリソースの大量投入によって、プロダクト開発が長期間止まりました。 さらにその後、SwiftUI のような新技術が登場したことで、結局は段階的な改善が必要になりました。 全面刷新の効果は一時的なものにとどまったわけです。 この経験から、現在は大規模な一括変更を避け、画面・機能単位での段階的な改善を続ける方針を取っています。 この「段階的である」という制約が、以降で述べる多くの設計判断の背景にあります。

## アーキテクチャ

### 全体構造

各画面を薄く保ち、状態管理とビジネスロジックを画面の外へ出すことで、画面単位で少しずつ移行できるようにしています。

* 画面遷移のロジックは後述の Coordinator が担い、画面 (ViewController / SwiftUI View) は表示に専念します。
* 画面の状態は、1 画面につき 1 つの Reactor が管理します。
* 多くの画面では、Reactor が端末内データベースの変更を購読し、変更に応じて UI を更新します。
* Reactor は KMP 側で実装され、Android とロジックを共有します。詳細は [KMP の章](https://docs.wantedly.dev/fields/apps/mobile-kmp)で扱います。

### レイヤーと責務

上位から下位への単方向の依存で、次のレイヤーに分かれます。

* UI: 画面の描画と入力の受け取り (UIViewController / SwiftUI View / Coordinator)
* Presentation (Reactor): UI 状態の管理と、ユースケースの呼び出し
* UseCase: アプリ固有のビジネスルール
* Repository: データソースの抽象化
* DataSource: API やデータベースとの実際のやり取り
* Entity: ドメインオブジェクトの定義

Reactor は Repository を直接呼ばず、必ず UseCase を経由します。 ビジネスロジックを UseCase に閉じ込めることで、再利用しやすく、テスト時に差し替えやすくなります。

### 状態管理の世代

単方向データフローという思想は共通しつつ、それを実現する実装には新旧 2 つの世代があります。 新旧が併存しているのは、全面刷新を避け、画面単位で段階的に移行しているためです。

| 世代 | UI                    | 状態管理          | 非同期     |
| -- | --------------------- | ------------- | ------- |
| 旧  | UIKit (No Storyboard) | ReactorKit    | RxSwift |
| 新  | SwiftUI               | KMP の Reactor | RxSwift |

* 画面ごとに UI 実装と状態管理を選べるため、UIKit の画面を SwiftUI と KMP の Reactor へ、画面単位で少しずつ移行できます。
* 新しく作る機能は、SwiftUI と KMP の Reactor の組み合わせを採用しています。旧世代の ReactorKit から KMP の Reactor へ移したのは、Android とビジネスロジックを共有するためです。
* 非同期処理は新旧どちらの世代も RxSwift が中心で、Swift Concurrency へ全面的には移行していません。

### 画面遷移

画面遷移には Coordinator パターンを用います。 Coordinator パターンは、画面遷移のロジックを画面 (ViewController / View) から切り離し、専用のオブジェクトに集約する仕組みです。 画面が「次にどこへ遷移するか」を直接知らなくてよくなるため、画面の再利用性が高まり、遷移ロジックのテストや差し替えが容易になります。

* 各画面・各フローに対応する Coordinator を用意し、アプリ起動時の起点となる Coordinator から順に構築します。
* モジュールをまたぐ遷移は、遷移の生成を一箇所に集約する窓口を通します。機能モジュール同士が互いに直接依存せずに他機能の画面へ遷移できるようにするためです。
* プッシュ通知や外部リンクなど、アプリ外部からの遷移は URL を遷移リクエストに解決して扱います。

### DI（依存性注入）

依存性注入は、オブジェクトが必要とする依存を外から渡す仕組みです。 結合度を下げて実装を差し替えやすくし、テストを書きやすくします。

* 依存性注入には Swinject を用います。
* 依存の登録は役割ごとに複数の Assembly に分けて定義し、起動時に Assembler で 1 つのコンテナにまとめます。テストやスタブ用に Assembly を丸ごと差し替えられます。

### UI 実装の方針

* UI は SwiftUI へ移行中です。宣言的 UI により画面実装を簡潔にでき、Apple 標準の方向性にも追随できるためです。新規の画面は SwiftUI で実装し、UIKit 画面の新規追加は避けます。
* 独自実装ではなく、デザインシステムのコンポーネントを優先して使います。デザインシステムの詳細は「デザインシステム」の章を参照してください。
* 既存の UIKit ベースの画面も、修正の機会にあわせて SwiftUI へ置き換えていきます。

## モジュール構成

モジュールを分割する狙いは、変更の影響範囲を絞ってビルド時間を抑え、依存を整理して全体の見通しを良くすることです。

* アプリのコードは、ローカルの Swift Package である `VisitPackage` に集約し、機能とレイヤーでモジュールを分割しています。
* KMP の共有コードは、1 つのモジュールとして取り込みます。KMP 自体は内部的にマルチモジュールですが、iOS からは 1 つのまとまったモジュールとして扱われる性質があるためです。
* サードパーティ依存は Swift Package Manager (SPM) に一本化しています。
* Xcode プロジェクトのファイルは XcodeGen で生成し、プロジェクト設定の差分をレビューしやすくしています。

## テスト戦略

テストはテスティングピラミッドの考え方に沿って構成します。 実行が速く安定したユニットテストを土台として厚く積み、実行コストが高く壊れやすい UI テストは上位に絞ります。 フィードバックの速さと安定性を保つためです。

* ユニットテスト（土台）: Quick / Nimble で記述します。
* モック: Cuckoo を用います。
* UI / E2E テスト（上位）: XCUITest に加え、ノーコードの E2E テスト自動化サービス MagicPod を用います。

自動テストに加えて、実機での動作確認も行っています。

## 歴史

iOS の実装は、おおまかに次の順で世代を重ねてきました。

1. Swift + UIKit による ReactorKit（2018 年の全面リニューアル）
2. KMP の Reactor（2021 年、Android とのロジック共有）
3. SwiftUI（2022 年、宣言的 UI への移行）

詳細な世代の遷移は [モバイルネイティブアプリについて](https://docs.wantedly.dev/fields/apps/mobile-native-app) の章に譲ります。

## 話を聞きに行きたい

* Slack: [#mobile\_chapter](https://wantedly.slack.com/archives/C014VN3KKB9)

## もっと知りたい

* [KMP の章](https://docs.wantedly.dev/fields/apps/mobile-kmp)（本 Handbook 内。ロジック共有と Reactor の詳細）
* [モバイルネイティブアプリについて](https://docs.wantedly.dev/fields/apps/mobile-native-app) の章（本 Handbook 内。各 OS の世代の全体像）
* [wantedly/visit-app](https://github.com/wantedly/visit-app) リポジトリ (internal。iOS のアーキテクチャや SwiftUI の実装規約を含む)


# Android

想定読者:

* Android アプリの開発を担当するモバイルエンジニア
* iOS を主とし、Android 側の構成を把握したいモバイルエンジニア

本章では Wantedly の Android アプリのアーキテクチャを扱います。 ここでは Android ネイティブに閉じた話を対象とし、個別の実装やコードの詳細には踏み込まず、全体像を素早くつかめることを目的とします。

モバイルアプリの開発では、新しい技術の登場やプロダクトの課題の変化にあわせて、アーキテクチャは必然的に世代交代していきます。 全面的に作り直して一度きれいにしても、その効果は長続きせず、いずれまた段階的な改善を迫られます。

だからこそ、個々のアーキテクチャや実装そのものよりも、その選択の背後にある思想・戦略を理解することが重要です。 本章では Wantedly の Android アプリの構成を具体的に示しながら、なぜその形を選んでいるのかという考え方を中心にまとめます。

## 概要 — 設計思想

Wantedly の Android アプリは次の原則に基づいて設計されています。

* 段階的リニューアル: 全面刷新は行わず、画面・機能単位で少しずつ置き換える
* 単方向データフロー (UDF): 状態の流れを一方向に保ち、挙動を予測可能にする
* Single Source of Truth: データベースを唯一の情報源とし、画面はそれを購読する
* マルチモジュール: 役割と機能でモジュールを分割し、規模の拡大に耐える
* ロジックの共有: ビジネスロジックを Kotlin Multiplatform (KMP) で iOS と共通化する
* 標準的な仕組みに寄せる: 独自実装より、AndroidX のような広く使われている標準やイディオムを優先する
* 移行を前提とした運用: 新規実装は新しい方式に寄せ、古い方式の新規追加を避ける

標準的な仕組みに寄せるのは、汎用的なスキルで開発でき、新しく参加する人にも学びやすく、AI による開発支援とも相性が良いためです。 標準に追随することで、将来の技術変化にも対応しやすくなります。

過去に iOS の全面リニューアルで開発が長期間止まった経験から、Android では大規模な一括変更を避け、段階的な改善を続ける方針を取っています。 iOS もこの全面リニューアルを経験しましたが、その後 SwiftUI のような新技術が登場したことで結局は段階的な改善が必要になり、リニューアルの効果は一時的なものにとどまりました。 この「段階的である」という制約が、以降で述べる多くの設計判断の背景にあります。

## アーキテクチャ

### 全体構造

* 画面は Single Activity + Multiple Fragments で構成します。Activity はサインイン・オンボーディング用と、ログイン後用の 2 系統です。
* 1 つの画面（Fragment）に対して 1 つの ViewModel を配置します。
* ViewModel はデータベースの変更を購読し、変更に応じて UI を更新します。
* ViewModel が担う状態管理の実装は世代交代の途中で、現在は KMP の Reactor へ移行しています（詳細は後述の「状態管理の世代」）。

### レイヤーと責務

上位から下位への単方向の依存で、次のレイヤーに分かれます。

* UI: 画面の描画と入力の受け取り
* ViewModel（Presentation）: UI 状態の管理と、ユースケースの呼び出し（状態管理の実装の世代は後述）
* UseCase: アプリ固有のビジネスルール
* Repository: データソースの抽象化
* DataSource: API やデータベースとの実際のやり取り
* Entity: ドメインオブジェクトの定義

ViewModel は Repository を直接呼ばず、必ず UseCase を経由します。 ビジネスロジックを UseCase に閉じ込めることで、再利用しやすく、テスト時に差し替えやすくなります。

### 状態管理の世代

単方向データフローという思想は共通しつつ、それを実現する実装には新旧 2 つの世代があります。

| 世代    | UI              | 状態管理                  | 非同期                    |
| ----- | --------------- | --------------------- | ---------------------- |
| 旧     | XML + Epoxy     | Flux-like な ViewModel | RxJava + LiveData      |
| 新（最新） | Jetpack Compose | KMP の Reactor         | Coroutines の StateFlow |

* どちらの世代も、画面のルートで状態を購読するという同じ作法に載せています。そのため画面単位で少しずつ移行できます。
* 新しく作る機能は、例外なく Compose と KMP の Reactor の組み合わせを採用しています。
* KMP の Reactor は iOS とロジックを共有するための中心です。詳細は [KMP の章](https://docs.wantedly.dev/fields/apps/mobile-kmp)で扱います。

### 画面遷移

* 画面遷移には Jetpack Navigation を用います。遷移の定義は機能ごとに分割して管理します。
* モジュールをまたぐ遷移や、アプリ外部からの遷移を型安全に扱うため、独自の仕組みを用意しています。Jetpack Navigation は kotlinx.serialization のサポートにより型安全な遷移が可能になりましたが、そこへ移行する前のため、同等の仕組みを自前で構築しています。
* 画面の描画は Compose へ移行していますが、遷移の土台は Fragment ベースの Navigation を維持しています。

### DI（依存性注入）

依存性注入は、オブジェクトが必要とする依存を外から渡す仕組みです。 結合度を下げて実装を差し替えやすくし、テストを書きやすくします。

* 依存性注入には Dagger Hilt を用います。
* 手書きのコンポーネント定義は持たず、Dagger Hilt が用意する標準のスコープ階層（アプリ全体・画面・ViewModel など）に素直に乗せます。
* 各機能モジュールが自分の依存グラフを自律的に組み立てるため、モジュールを増やしても DI の設定が一箇所に集中しません。

### UI 実装の方針

* UI は Jetpack Compose へ移行中です。新規の画面は Compose で実装し、新しい XML リソースの追加は避けます。
* 独自実装ではなく、Material Components をベースとしたデザインシステムのコンポーネントを優先して使います。デザインシステムの詳細はデザインシステムの章を参照してください。
* 既存の View ベースの画面も、修正の機会にあわせて Compose へ置き換えていきます。

## モジュール構成

モジュールを分割する主な狙いは、ビルド時間の短縮（変更した箇所だけを再ビルド・並行ビルドできる）と、依存関係を単方向に保って全体の見通しを良くすることです。

* 数十のモジュールから成るマルチモジュール構成です。役割（レイヤー）による分割と、機能（feature）による分割を組み合わせ、規模の拡大に耐えられるようにしています。
* 依存には規律を設けています。同じグループ内のモジュール同士が横方向に依存することを禁止し、循環依存を防ぎます。たとえば、ある機能モジュールが別の機能モジュールへ直接依存することは避けます。
* 複数の機能で共通して使う UI モジュールは、データモデルに依存させません。UI 部品の再利用性を保つためです。
* KMP の共有コードは、1 つのモジュールとして取り込みます。KMP 自体は内部的にマルチモジュールですが、iOS からは 1 つのまとまったモジュールとして扱われる性質があるため、Android でも同様に単一のモジュールとして依存します。
* ビルド設定・DI・Compose の導入といった横断的な設定は、共通のビルド規約（convention plugin）にまとめて各モジュールへ適用します。設定の再利用性を高め、モジュールごとの build 設定を最小限に抑えるためです。

## テスト戦略

テストはテスティングピラミッドの考え方に沿って構成します。 実行が速く安定したユニットテストを土台として厚く積み、実行コストが高く壊れやすい UI テストは上位に絞ります。 フィードバックの速さと安定性を保つためです。

* ユニットテスト（土台）: JVM 上で実行します。Android 依存が必要な箇所は Robolectric を用います。
* UI テスト（上位）: 実機で実行します。
* モック: MockK を用います。
* テストの命名は Given-When-Then の形式にそろえます。

## 歴史

Android の実装は、おおまかに次の順で世代を重ねてきました。

1. Java + XML による MVC
2. Kotlin + Flux-like な ViewModel（単方向データフローの導入）
3. KMP の Reactor（iOS とのロジック共有）
4. Jetpack Compose（宣言的 UI への移行）

詳細な世代の遷移は [モバイルネイティブアプリについて](https://docs.wantedly.dev/fields/apps/mobile-native-app) の章に譲ります。

## 話を聞きに行きたい

* Slack: [#mobile\_chapter](https://wantedly.slack.com/archives/C014VN3KKB9)

## もっと知りたい

* [KMP の章](https://docs.wantedly.dev/fields/apps/mobile-kmp)（本 Handbook 内。ロジック共有と Reactor の詳細）
* [モバイルネイティブアプリについて](https://docs.wantedly.dev/fields/apps/mobile-native-app) の章（本 Handbook 内。各 OS の世代の全体像）
* [wantedly/visit-app](https://github.com/wantedly/visit-app) リポジトリ (internal。Android のアーキテクチャや Compose の実装規約を含む)


# Kotlin Multiplatform (KMP)

## 導入背景と現状

KMPは2021年からWantedly アプリに本格導入され、現在はビジネスロジックの共有において中核的な役割を果たしています。 導入の経緯については、[React Nativeをやめる話とKotlin Multiplatform](https://www.wantedly.com/companies/wantedly/post_articles/282562)で詳しく解説されています。

## 現在の導入状況

KMPはWantedly アプリに導入され、iOS / Android間でビジネスロジック（Reactor実装、API通信、データベース操作など）を共有しています。 2025年12月に、それまで別々だったリポジトリを単一のモノレポ `wantedly/visit-app` に統合しました。

**リポジトリ構成**

```
visit-app/
├── shared/      # iOS / Androidで共有するKMPコード（旧 visit-app-shared）
├── iosApp/      # iOSアプリ
└── androidApp/  # Androidアプリ
```

**モノレポ化による改善**

* KMPの変更がアプリに反映されるまで20分以上かかっていたのが、即座に反映されるようになった
* 破壊的変更を同一PR内で検知できるようになった（従来は別リポジトリのCI実行まで気づけなかった）
* iOS / Androidの変更を1つのPRでアトミックに行えるようになった

モノレポ化の詳細は [Wantedly のモバイルアプリのモノレポ化について書かれた記事](https://www.wantedly.com/companies/wantedly/post_articles/1039908) を参照してください。

## KMPの方針

**Wantedly アプリ**: 積極的な導入を推進

* 新機能開発時はKMP Reactorの採用を優先
* 既存機能の段階的な移行を実施

**全社展開**: 慎重な検討段階

* 認証処理などの共通性が高い機能から実験的に導入
* 各アプリの特性を考慮した段階的な適用

## KMPのアーキテクチャ

### 設計思想

KMPでは、既存のReactorKitパターンを踏襲した独自のReactorアーキテクチャを採用しています。 これにより、iOS / Android開発者の学習コストを最小限に抑えながら、プラットフォーム間でのビジネスロジック共有を実現しています。

![KMPアーキテクチャ](/files/-MhWAONUXSOOZaWjmbS7)

### モジュール構成

**shared の主要モジュール**

* `:api`: API通信関連（REST/GraphQL）
  * `:entity`: APIエンティティ定義
  * `:graphql`: GraphQL クライアント（Apollo）
* `:core`: 共通ユーティリティ
* `:visit-app-shared`: メインモジュール（Reactor実装）
* `:testing`: テスト用ユーティリティ

### 技術スタック

* データベース: SQLDelight（型安全なSQL）
* API通信: Ktor Client + Apollo GraphQL
* 非同期処理: Kotlin Coroutines
* 依存性注入: 手動DI（DIフレームワークには依存しない）
* テスト: Kotest
* iOS連携: SKIE（Kotlin の共有コードを Swift から自然に扱えるようにする相互運用ツール）

### iOS 連携（SKIE）

Wantedly アプリでは [SKIE](https://skie.touchlab.co/) を導入し、KMP の共有コードを Swift にとって自然な形で公開しています。主に次の用途で活用しています。

* **sealed class の網羅的な switch**: Swift 側で全ケースを網羅的に分岐でき、分岐漏れをコンパイル時に検出できる
* **suspend 関数の async/await 化**: Kotlin の `suspend` 関数を Swift の async/await として自然に呼び出せる
* **デフォルト引数・enum のブリッジ**: Kotlin のデフォルト引数や enum を Swift から自然に扱える

## Reactor

### 概要

ReactorはReactorKitを参考に開発された、KMP環境での単方向データフローを実現するフレームワークです。 iOS / Android両プラットフォームで一貫したアーキテクチャパターンを提供し、予測可能な状態管理を実現します。

### データフロー

![Reactor](/files/-MhWAONVlOWyLLcfSjIg)

**基本的なデータフロー**

1. **Action**: UIからのユーザーインタラクション（タップ、入力など）
2. **Mutate**: `Action`を受けてAPI通信などの副作用を実行し、`Mutation`を生成
3. **Reduce**: `Mutation`を受けて現在の`State`を新しい`State`に変換
4. **State**: UIが監視する状態オブジェクト、変更時にUI更新をトリガー

### State の設計

State は 2 種類の情報で構成します。

* **モードに関わらず常に持つ情報**（例: 一時的に表示するエラートースト）→ **data class** のフィールドにする
* **画面のモード**（Loading / Succeeded / Error のいずれか一つ）→ **sealed class** にし、各モードに固有のデータはそのサブタイプに持たせる

shared（KMP）で定義するのは State だけです。State を UI に反映する処理は、後述のとおり iOS / Android の各プラットフォーム側が担います。

下記は、ある一覧画面（会社一覧など）を例にした最小の State 定義です。

```kotlin
// shared（KMP）で定義する State
// 画面全体の State は data class（常に存在する付随情報を持てる）
data class State(
    val state: ScreenState = ScreenState.Loading,
    val toastError: Throwable? = null, // 主状態とは独立に一時表示する付随情報
)

// 排他的な画面モードは sealed interface（和の型）で表現
sealed interface ScreenState {
    data object Loading : ScreenState

    // 成功時だけ、一覧本体とページネーションに必要な情報をまとめて持つ
    data class Succeeded(
        val items: List<String>,        // 表示するリスト本体
        val hasNextPage: Boolean,       // 次ページが存在するか
        val isNextPageLoading: Boolean, // 追加読み込み中か（末尾のスピナー表示に使う）
        val nextPageCursor: String?,    // 次ページ取得の起点（なければ null）
    ) : ScreenState

    data class Error(val cause: Throwable) : ScreenState // 失敗時だけ原因を持つ
}
```

`hasNextPage` / `isNextPageLoading` / `nextPageCursor` は成功後にしか意味を持たない情報なので、`Loading` や `Error` からは触れないよう `Succeeded` の中に閉じ込めています。追加読み込みは「新しい画面モードに遷移する」のではなく、`Succeeded` のまま `copy` でフラグを更新することで表現します。

**なぜ sealed class で分けるのか**

* **網羅性の強制**: Kotlin の `when` / Swift の `switch` が全サブタイプの分岐を要求するため、状態を追加したときの「対応漏れ」がコンパイルエラーで検出できる。
* **不正な状態アクセスの排除**: `Succeeded` だけが一覧データを、`Error` だけが原因を持つため、「ローディング中なのに一覧を参照する」といったアクセスを型レベルで防げる。
* **Declarative UI との相性**: 「状態 → UI」を宣言的な一対一マッピングとして書ける。

なお「追加読み込み中」のような成功状態の中の細かな進捗は、新たなサブタイプを増やすのではなく `Succeeded` のフィールド（フラグや cursor）として持たせることで、状態の数を最小限に保っています。

### 実装詳細

詳細な実装については以下を参照：

* [Wantedly VisitにおけるKotlin Multiplatformの導入と実装](https://www.wantedly.com/companies/wantedly/post_articles/300999)
* [wantedly/visit-app リポジトリ](https://github.com/wantedly/visit-app)（`shared/` 配下に KMP の共有コード）

## Database as Single Source of Truth

### 設計原則

KMPアーキテクチャでは、SQLDelightを使用したデータベースをSingle Source of Truthとして採用しています。 この設計により、データの一貫性を保ちながら、複数画面間でのリアルタイムな状態同期を実現しています。

### 技術実装

**SQLDelight の特徴**

* 型安全性: SQLクエリのコンパイル時検証
* マルチプラットフォーム: iOS / Android共通のDB操作
* Reactive: Kotlin Coroutines Flowによるリアクティブクエリ
* パフォーマンス: ネイティブSQLiteの性能を活用

### データフロー例：ブックマーク機能

**依存関係**

![依存の流れ](/files/-MhWAONXcMozjGj0t6Zc)

各画面のReactorは、SQLDelightのFlowを通じてデータベースの変更を監視します。 破線は、データの購読（Observe）関係を表しています。

**データストリーム**

![データの流れ](/files/-MhWAONYCpyb0efLQmNl)

1. **初期データ取得**: 各画面が必要なデータをDBから取得
2. **リアルタイム監視**: DBの変更をFlowで監視
3. **自動UI更新**: データ変更時に自動的にUI更新

**状態変更フロー**

![ブックマークのフロー](/files/-MhWAONZ56HoyHWxqcjs)

1. **ユーザーアクション**: 詳細画面でブックマークボタンをタップ
2. **API通信**: サーバーにブックマーク状態を送信
3. **DB更新**: レスポンス受信後、ローカルDBを更新
4. **自動伝播**: DB変更が全ての購読者（画面）に自動通知
5. **UI反映**: 一覧画面と詳細画面の両方で即座にUI更新

### 実装上の利点

* データ整合性: 単一のデータソースによる一貫した状態管理
* パフォーマンス: ローカルDBによる高速なデータアクセス
* オフライン対応: ネットワーク状況に依存しない基本機能
* 開発効率: プラットフォーム間でのロジック共有

### GraphQL統合

**導入背景** Wantedlyでは開発効率とパフォーマンス向上のためにGraphQLを採用しており、新しい画面は基本的にGraphQLで実装しています。 詳細は[GraphQL Gateway - アプリ向けにAPIを公開する](https://docs.wantedly.dev/fields/the-system/graphql-gateway)を参照してください。

**KMPでの実装**

* クライアント: [Apollo GraphQL](https://www.apollographql.com/) Kotlin版
* コード生成: GraphQLスキーマからKotlinコードを自動生成
* 型安全性: クエリとレスポンスの型安全な操作
* キャッシュ: Apollo Clientの自動キャッシュ機能

**開発方針**

* 新機能: GraphQLを優先的に採用
* 既存機能: REST APIから段階的に移行
* パフォーマンス: 必要なデータのみを取得（Over-fetching回避）

## 話を聞きに行きたい

* Slack: [#mobile\_chapter](https://wantedly.slack.com/archives/C014VN3KKB9)

## もっと知りたい

* [モバイルネイティブアプリについて](https://docs.wantedly.dev/fields/apps/mobile-native-app) の章（本 Handbook 内。各 OS の世代の全体像）
* [iOS の章](https://docs.wantedly.dev/fields/apps/mobile-ios) / [Android の章](https://docs.wantedly.dev/fields/apps/mobile-android)（本 Handbook 内。各プラットフォームでの利用）
* [wantedly/visit-app](https://github.com/wantedly/visit-app) リポジトリ (internal。KMP の共有コードを含む)


# The System


# protobuf スキーマと gRPC 通信

ウォンテッドリーでは主に以下の3種類のプロトコルを通信に使っています。

* gRPC + protobuf
* GraphQL
* HTTP + JSON

それぞれの利点・欠点を踏まえて、2024年現在は以下のような使い分けがなされています。

* マイクロサービス間通信
  * 原則として **gRPC + protobuf** を使う。
  * 昔の名残りで HTTP + JSON も使われている。gRPC + protobufへの移行中
* Webアプリ・モバイルアプリとシステムとの通信
  * 原則として **GraphQL** を使う。
  * 昔の名残りで HTTP + JSON も使われている。GraphQLへの移行中

本稿では gRPC + protobuf の入門とウォンテッドリーにおけるベストプラクティスを紹介します。

## protobufとgRPC

**protobuf (Protocol Buffers)** はデータフォーマットで、JSONの役割を置き換えるものです。一方 **gRPC** は通信プロトコルで、HTTPの役割を置き換えるものです。

gRPC + JSON や HTTP + protobuf のような組み合わせも可能ですが、ウォンテッドリーでは使わないので以降では考えません。

JSONとprotobufの重要な違いとして、protobufは**フォーマットがスキーマに依存する**という点があります。JSONはスキーマがなくても完全なシリアライズ・デシリアライズが可能ですが、protobufのデータをシリアライズ・デシリアライズするにはスキーマ情報が必要です。gRPCは技術的には必ずしもスキーマ依存ではありませんが、実装上はスキーマなしで実装するのは困難です。

この技術的制約により**スキーマファースト開発が強制される**のが protobuf + gRPC の強みのひとつです。スキーマファーストであることによって以下のような利点があります。

* サーバーとクライアントが実装を並行して進めることができる。
* データに関する暗黙の知識を言語化し、共有する機会になる。

protobuf を利用している、より詳しい背景についてはブログ記事 [Protocol Buffers によるプロダクト開発のススメ - API 開発の今昔 -](https://www.wantedly.com/companies/wantedly/post_articles/309513) を参照してください。

## protoファイル

protobufのスキーマは**protoファイル**という専用のフォーマットで記述します。このファイルには**protobufのスキーマに加えて、gRPCのAPI定義も記載することができます**。

```protobuf3
// users.proto

syntax = "proto3";
package wantedly.users;

// protobufのスキーマ定義
message User {
  // = の後に書かれているのは初期値やデフォルト値ではなく、「タグ」という背番号。
  uint64 id = 1;
  string name = 2;
}

message GetUserRequest {
  uint64 id = 1;
}

// gRPCのAPI定義
service UsersService {
  rpc GetUser(GetUserRequest) returns (User) {}
}
```

protoファイルを作ったら、これを各プログラミング言語の実装に変換する必要があります。これはprotocというコンパイラを使って行います。

protoc本体が対応しているのはC++, C#, Java, JavaScript, Objective-C, PHP, Python, Rubyの7言語だけですが、それ以外の言語でもプラグインを用意することでコンパイルが可能です。たとえばGoはprotoc本体に同梱されていないだけで、Go側から使える `google.golang.org/protobuf` という公式ライブラリにコンパイラが組込まれています。

たとえば、Rubyへのコンパイルは以下のようにして行えます。

```
# users_pb.rb が生成される
protoc users.proto --ruby_out=.
```

出力されたコードにはクラス定義が含まれていて、メッセージオブジェクトを生成してシリアライズすることができます。

```ruby
irb(main):001:0> require './users_pb'
=> true
irb(main):002:0> user = Wantedly::Users::User.new(id: 42, name: "Tanaka Taro")
=> <Wantedly::Users::User: id: 42, name: "Tanaka Taro">
irb(main):007:0> user.to_proto
=> "\b*\x12\vTanaka Taro"
```

## protoファイルとJSON API

protoファイルにprotobufのスキーマ定義とgRPCのAPI定義を書けることを紹介しましたが、実はprotoファイルには**JSONのスキーマ定義とHTTPのAPI定義**を書くこともできます。

```protobuf3
// JSONへの対応付けは自動的に生成される
// この例では { "id": "42", "name": "Tanaka Taro" } にマッピングされる
message User {
  uint64 id = 1;
  string name = 2;
}

// serviceをHTTPにマッピングするには追加のアノテーションが必要
service UsersService {
  rpc GetUser(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/api/users/{id}"
    };
  }
}
```

これにより gRPC + protobuf で実装されたAPIを HTTP + JSON に自動変換して提供することができます。この機能はgrpc-gatewayというGoのライブラリで実際に実装されていますが、gRPCとGraphQLが標準になった現在では非推奨となります。 2024年現在、ウォンテッドリーでは一部のモバイル向けAPIをこの方法で提供しています。

## APIs (wantedly/apis)

スキーマ・API定義はマイクロサービス間で共有されてはじめて意味のあるものです。ウォンテッドリーではAPI定義のための中央リポジトリ wantedly/apis を用意することで定義の共有を実現しています。

wantedly/apis に変更を加えると、従属する以下のリポジトリが自動的に更新されます。

* wantedly/apis-ruby
* wantedly/apis-go
* wantedly/apis-python
* etc.

各マイクロサービスでこれらのパッケージに依存し、必要に応じて更新することで、APIの変更を一貫した形で反映することができるようになっています。

## 互換性

マイクロサービスアーキテクチャではマイクロサービス境界はチーム境界と対応するのが原則ですから、API定義は他チームとの間の規約に他なりません。そのため、APIに変更を加えるときは互換性を保つのが大原則です。 (まだ本番投入されていないことが明らかな場合など、例外的に互換性を壊す判断をすることもあります)

protoファイルにおける「互換性」には2種類の視点があります。ひとつは**スキーマ・通信APIの互換性**です。もうひとつは**ライブラリとしてのAPI**の互換性です。とはいえ、継続的な更新を可能にするために、いずれの互換性も保つことが望ましいです。

### スキーマ・通信APIの互換性

たとえば、以下のような変更をすると正しく通信できなくなってしまいます。

```protobuf3
message User {
  // 型を変更した
  fixed64 id = 1;
  // タグを変更した
  string name = 3;
}
```

このような事態を防ぐためにも、型とタグは変更してはいけません。例外として互換性のある型はいくつかありますが、本稿では詳細は述べません。oneofやenumの互換性についても気をつけるべき点がいくつかありますが、必要に応じて[proto3のLanguage Guide](https://developers.google.com/protocol-buffers/docs/proto3)を参照するといいでしょう。

gRPCではメソッドを修飾名で区別するので、パッケージ名を含めた名前の変更も基本的には行ってはいけません。

状況次第で以下のような追加の要件もあります。

* Any型を使っているとき
  * Any型に登場するmessageの修飾名を変えてはいけません。
* JSONマッピングを使っているとき
  * フィールド名を変えてはいけません。

### ライブラリとしての互換性

生成されるライブラリのAPIが変わるような変更も極力避けましょう。典型的にはフィールド名の変更がこれに当たります。

### 非推奨化と予約

フィールドを安全に削除するために、非推奨化と予約という機能があります。

**非推奨化 (deprecation)** はフィールド等を実際には削除せずに警告扱いにする方法です。JavaやGoなどでは警告用のアノテーションがつけられるのでコンパイル時に古いコードを発見することができます。

```protobuf3
message User {
  uint64 id = 1;
  // 非推奨。かわりにprofile.nameを使ってください。
  string name = 2 [deprecated = true];
  Profile profile = 3;
}
```

**予約 (reservation)** は番号が誤って再利用されるのを防ぐための仕組みです。

```protobuf3
message User {
  uint64 id = 1;
  // 将来間違って2を再利用しないようにreserveしておく
  reserved 2;
  // JSONマッピングを使っている場合、フィールド名も予約するとよい
  reserved "name";
  Profile profile = 3;
}
```

API定義は全てのマイクロサービスで継続的に更新するのが望ましいですが、実際にはそうならないこともあります。その場合、とても古いAPI定義を使っているマイクロサービスと、最新のAPI定義を使っているマイクロサービスが共存する可能性があります。これらのマイクロサービスが2番のタグを別の意味で使っていた場合に何らかの事故が起こる可能性があるので、できるだけreservedを使うようにしましょう。

タグは15以下、2047以下でそれぞれ1バイトずつ節約できる点を除けば、基本的にどの番号でも違いはありません。無理に詰めようとせず、互換性を最大限に保つ形で割り当てましょう。

## nullとoneofの扱い

JSONから入ってきた人にとって戸惑いのもとになりがちなのが、nullとoneofの扱いです。

protobufにはnullという値はなく、フィールドが存在しないことをnullとして表現することがあります。ところが、フィールドが存在しない場合でも別のデフォルト値が使われることもあり、その規則は以下のように複雑です。

* repeatedの場合: 空配列 (`[]`)
* mapの場合: 空マップ (`{}`)
* repeatedでもmapでもない場合
  * ネストしたメッセージの場合: `null`
  * スカラー型 (プリミティブ型、enum型) の場合
    * oneofの一部の場合: `null`
    * optionalの場合 (protoc 3.15.0以降の機能): `null`
    * oneofでもoptionalでもない場合: プリミティブ型のデフォルト値が使われる
      * 整数型: `0`
      * 浮動小数点数型: `0.0`
      * bool型: `false`
      * 文字列・バイト列: 空文字列 (`""`)
      * enum: enumの最初の値 = `0`

なお、mapのキーが省略された場合はスカラー型の規則に従い、値が省略されたときの挙動は実装依存です。

nullがあるかないかはAPI定義においては重要な関心のひとつなので、大変ですが上の規則は覚えてしまうのがいいでしょう。

nullをうまく表明するテクニックは以下の通りです。

* nullが欲しいのにない場合
  * protoc 3.15.0以降ではoptionalが使えます。
  * プリミティブ型に関しては、 `google.protobuf.StringValue` などのラッパー型が提供されています。
  * enumやrepeated/mapにnullが必要ならmessageでラップするのがいいでしょう。ただし、enumは最初の値をnull相当の値として扱うべしとされているので、nullが必要な状況はコードスメルかもしれません。
* nullの可能性を排除したい場合
  * ネストしたメッセージがnullでないことを表明する構文はありません。
  * ウォンテッドリーでは `// Required` というコメントをつけています。アノテーションを用意する場合もあるようです。

oneofは直和的なデータを表現するための道具です。

```protobuf3
message Notification {
  string message = 1;
  // image, video, userのうち1つを選択
  oneof rich_content {
    Image image = 10;
    Video video = 11;
    User user = 12;
  }
}
```

GoやRust(prost)では実際に直和的な表現になりますが、言語によっては**単なるoptionalフィールドの集まり**のように表現されます。 (ただし、複数の選択肢が同時に有効にならないような仕組みは通常あります) 実はprotobufのエンコーディング的にも、「単なるoptionalフィールドの集まり」として表現されています。また、JSONマッピングでも後者の解釈で表現されるので、気持ちの持ち方としてはこちらで考えておいたほうがいいでしょう。

また、protobufのoneofは **「どのフィールドもnull」という状態が許容されている** ので注意が必要です。このケースを考慮しておくことは、oneofのフィールドが増えたときの互換性の維持のためにも有用です。

## gRPC APIの基本

gRPC側は比較的シンプルで、名前、引数型、戻り値型の3つを決めることで新しいAPIが生やせます。

```protobuf3
service UsersService {
  rpc GetUser(GetUserRequest) returns (User) {}
  //  ^^^^^^^ 名前                      ^^^^ 戻り値型
  //          ^^^^^^^^^^^^^^ 引数型
}
```

引数型や戻り値型に `stream` という指定をつけることで、 Server Streaming / Client Streaming / BiDi Streaming という特別なモードにすることもできます。これはJavaScriptのジェネレーター関数のようなものをイメージするといいでしょう。使う機会は多くないですが、覚えておくと車輪の再発明を防げるかもしれません。

`rpc` の外側にある `service` はRPCの実装単位です。 `service` 内に複数の `rpc` がある場合、それらのrpcはまとめて実装することになります。Rubyでは `service` ごとに1つのクラスが生成されますし、 Goでは `service` ごとに1つのinterfaceが生成されます。

```protobuf3
// GetUserとListUsersはまとめて実装する
service UsersService {
  rpc GetUser(GetUserRequest) returns (User) {}
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {}
}

// これは別のサーバーで実装されるかもしれない。
service BooksService {
  rpc GetUser(GetBookRequest) returns (Book) {}
  rpc ListUsers(ListBooksRequest) returns (ListBooksResponse) {}
}
```

## gRPC APIの設計

gRPCの骨組みは比較的単純ですが、実際にAPIを設計しようとするとそれなりに自由度があることがわかります。この自由度を抑えつつできるだけよい設計に近づけるために、[GoogleのAPI Design Guide](https://cloud.google.com/apis/design)を参考にしています。

上記のAPIガイドラインに含まれない、より高度な提案は[Google API Improvement Proposals (AIPs)](https://google.aip.dev/general)というサイトにまとめられています。こちらも必要に応じて参照します。

本稿では特に指摘が起きやすい点をいくつか抜き出して説明します。

### 列挙型(Enum)

命名については下のように行うことが**推奨**されています。

* 列挙型: `UpperCamelCase`
* 列挙値: `CAPITALIZED_NAMES_WITH_UNDERSCORES`
* デフォルト値: `ENUM_TYPE_UNSPECIFIED`

列挙型は C++ と同じようなスコープになっているため、列挙値は列挙型が定義されているレベルでのスコープとなります。例えば次の定義では、**Sample1** と **Sample2** は同レベルに存在するため同じ **BAR** という名前を使用することができません。**Sample1** と **Test.Sample3** のようにレベルが違う場合は同じ名前を使用することができます。特にトップレベルに列挙型を定義する場合にはスコープに気を付けて定義してください。

```protobuf3
enum Sample1 {
  FOO = 0;
  BAR = 1;
}
enum Sample2 {
  BAR = 0;
  BAZ = 1;
}

message Test {
  emum Sample3 {
    BAR = 0;
  }
}
```

### リソース指向

Ruby on Railsを知っている人には馴染み深いかもしれませんが、リソースに注目してAPIを分割することが推奨されています。

たとえば、Ruby on Railsでは `BooksController` に `#index`, `#show`, `#delete` などのアクションを生やすことがあります。同様に、Google API Design Guideでも操作対象のリソースを `service` にして、その下に標準的な名前の `rpc` を置くことを[推奨しています](https://cloud.google.com/apis/design/standard_methods)。

```protobuf3
service BooksService {
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {}
  rpc GetBook(GetBookRequest) returns (Book) {}
  rpc CreateBook(CreateBookRequest) returns (Book) {}
  rpc UpdateBook(UpdateBookRequest) returns (Book) {}
  rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {}
}
```

**GetBook, CreateBook, UpdateBookが全てBookを返している**ことに注目してください。この部分がGetBookResponse, CreateBookResponseのようになっていたらコードスメルです。

### Input only, output only

リソース指向と関連する話ですが、Google API Design Guideでは同じリソース型を入出力で再利用することを推奨しています。

```protobuf3
message UpdateBookRequest {
  // 入力としてのBook
  Book book = 1;
  google.protobuf.FieldMask update_mask = 2;
}
```

入力で必要なフィールドと出力で必要なフィールドは必ずしも一致しないので、別のメッセージに分けてしまいたくなる人もいるかもしれませんが、区別することで得られるメリットは複雑性の増加に見合わないとGoogleは判断したのでしょう。ここは大人しくGoogleの経験に従うのがいいでしょう。

かわりに、リソース型の中で入力専用フィールドや出力専用フィールドに説明を加えることが推奨されています。

```protobuf3
message Book {
  uint32 id = 1;
  // Input only. 書籍の登録時にISBNを指定する。
  string isbn = 2;
  // Output only. 書誌情報から取得した題名。
  string title = 3;
  // Input only.
  // 書籍の表紙をユーザーがアップロードするときはS3のURLをここに入れる。
  string cover_image_url = 4;
}
```

### 標準フィールド名

[Standard Fields](https://cloud.google.com/apis/design/standard_fields) にフィールド名の命名規則が挙げられています。

命名は必ずしも合理性があって決まっているわけではないですが、長い物に巻かれるためにも迷ったら従っておくのがいいでしょう。

### エラーの表現

APIから詳細なエラー情報を返したくなることがあります。この場合のベストプラクティスは[Errors](https://cloud.google.com/apis/design/errors)にまとめられています。

リソース指向デザインを維持するため、またエラーを正しく各プログラミング言語のエラーにマップするために、エラーはgRPCのエラーの枠組みで返す (正常系のレスポンスの一部として返さない) ようにしてください。

gRPCのエラーには通常「ステータスコード」と「英語のエラーメッセージ」の2つが入っていますが、それに加えて任意のAny型のペイロードを入れられるようになっていて、エラーペイロードとして使うための標準的な型がいくつか定義されています。

### ページネーション

ページネーションにも推奨の方法があります。 Google API Design Guide の Design Patterns のページに [Pagination](https://cloud.google.com/apis/design/design_patterns#list_pagination) のセクションがあります。

### ドキュメンテーション

Protobuf のメッセージや Enum の定義では、すべてのフィールド・値に対して1行以上のコメントで、そのフィールドの説明を記述する必要があります（SHOULD）。

ウォンテッドリーのプロダクト開発では、Protobuf IDL で記述されたメッセージ定義・API スキーマを使ってバックエンドとフロントエンド（Web・モバイルアプリ）のエンジニアがコミュニケーションをします。 そのため、.proto ファイルにフィールドの説明・仕様・想定している使われ方などを明記しておくことで、円滑なコミュニケーションの助けになり、余計な確認や手戻りを大きく減らすことにつながります。

```protobuf3
// コメントの例
// 内部リンク: https://github.com/wantedly/apis/blob/master/wantedly/profile_page/link_collection.proto

message LinkCollection {
  // Required. プロフィールページ上に表示する SNS アカウントの一覧。
  // 他人のプロフィールページのときは、原則連係済みアカウントのみが表示される。
  // 自分のプロフィールページのときは、まだ連携していない SNS アカウントのプレースホルダが出る場合がある。
  // この一覧に含まれない SNS アカウントに関しては編集ハーフモーダル上にのみ表示される。
  // 代表的な例としては Google などがこれに当たる。
  // 表示順には影響しない。
  repeated wantedly.users.v2.SocialProfile.Provider displayed_social_providers = 3 [packed=false];
}
```

## gRPC 通信のエラーハンドリング

gRPCをリクエストする側は、サーバー側から返ってくるエラーを必要に応じてハンドリングしてあげる必要があります。

エラーのハンドリング方法としては以下のようなものがあります。

* リトライ
* フォールバック
  * 別の手段を使う（別APIを使うなど）
  * 空の値や既定値を返す
* エスカレーション（エラーを受け止めた上で別の形にして呼び出し元に失敗を伝える）

2回までリトライして、それでもダメならフォールバックして空の値を返す、のようにこれらの手段を組み合わせる場合もあります。

以下に、各エラーに対するウォンテッドリーで推奨されるハンドリングポリシーを示します。 ここで未定義とは、社内でまだ議論が十分なされておらず統一的なポリシーが確定していないことを示します。 各エラーの内容については [Handling Errors](https://cloud.google.com/apis/design/errors#handling_errors) を参照してください。

| エラーの種類               | ハンドリングポリシー                                                                |
| -------------------- | ------------------------------------------------------------------------- |
| INVALID\_ARGUMENT    | 未定義                                                                       |
| FAILED\_PRECONDITION | 未定義                                                                       |
| OUT\_OF\_RANGE       | 未定義                                                                       |
| UNAUTHENTICATED      | 未定義                                                                       |
| PERMISSION\_DENIED   | 未定義                                                                       |
| NOT\_FOUND           | 未定義                                                                       |
| ABORTED              | 未定義                                                                       |
| ALREADY\_EXISTS      | 未定義                                                                       |
| RESOURCE\_EXHAUSTED  | 未定義                                                                       |
| CANCELLED            | 未定義                                                                       |
| DATA\_LOSS           | 未定義                                                                       |
| UNKNOWN              | 当該RPCの成功が、現在実行中の処理の継続に必要でない場合、実行中の処理を中断させないようにする。                         |
| INTERNAL             | 当該RPCの成功が、現在実行中の処理の継続に必要でない場合、実行中の処理を中断させないようにする。                         |
| NOT\_IMPLEMENTED     | 未定義                                                                       |
| UNAVAILABLE          | rpc の内容によらず確率的に発生しうるため、基本的にハンドリングする。ハンドリングの方法はリトライ、フォールバック、エスカレーションのいずれか。 |
| DEADLINE\_EXCEEDED   | rpc の内容によらず確率的に発生しうるため、基本的にハンドリングする。ハンドリングの方法はリトライ、フォールバック、エスカレーションのいずれか。 |

## その他ウォンテッドリーで利用しているツール群

* Linter / Formatter
  * <https://github.com/bufbuild/buf>
  * <https://github.com/yoheimuta/protolint>
* デバッグ
  * <https://github.com/ktr0731/evans>
  * <https://github.com/rerost/giro>
    * [社内での利用方法(internal)](https://github.com/wantedly/apis-reflection-server)
* GraphQL Gatewayへの型変換
  * <https://github.com/proto-graphql/proto-graphql-js>

#### 話を聞きに行きたい

* Slack: [#backend\_chapter](https://wantedly.slack.com/archives/C01GY4RF5BK), [#microservices](https://wantedly.slack.com/archives/CARTLEXR9)

#### もっと知りたい

* <https://github.com/wantedly/apis>


# 実践: gRPC in Ruby

本章では、[gRPC in Ruby の Quick Start](https://www.grpc.io/docs/quickstart/ruby/) から一歩先、「ウォンテッドリーで Ruby を使って gRPC Server/Client の開発をどう行なっているのか」について紹介します。

## Ruby で gRPC Server を作る

### Rails としてのディレクトリ構成

まずはディレクトリ構成について説明します。基本的に Ruby on Rails の上で作ることを想定しています。

```
.
├── app
│   ├── grpc_services
│   ├── models
│   └── pb_serializers
├── bin
├── config
└── db
```

特徴的な部分を紹介します。

* `app/grpc_services`
  * protobuf から生成したサービスクラスを継承したサービスクラスを置きます
  * ファイル名およびクラス名にはそれぞれ `_grpc_service`, `GrpcService` という suffix を付与します
* `app/pb_serializers`
  * Protobuf Message object を生成する pb-serializer の実装クラスを配置します
  * ファイル名およびクラス名にはそれぞれ `_pb_serializer`, `PbSerializer` という suffix を付与します

### Rails としてのアーキテクチャ

このセクションは主に Rails 経験者のための解説です。先ほどのディレクトリ構成の背後にあるアーキテクチャと、デフォルトの Rails との差分について軽く触れておきます。図を見てもらいつつ、二段階に分けて説明します。

![](/files/XACOk1sCpbYjy1m8Z75E)

まず、デフォルトの Rails の MVCアーキテクチャ（①）に対して、JSON / HTTP を話す API サーバーにするために ActionView を取り去って ActiveModelSerializers などのシリアライズ機構を導入します。

次に、②のアーキテクチャに対して、Protobuf / gRPC を話せるように各責務に対応するコンポーネントを差し替えたのがウォンテッドリーにおける gRPC サーバーの推奨のアーキテクチャ（③）です。

一見大きく変わったように見えるかもしれませんが、モデルやシリアラズなどの概念は同じなので案外馴染みやすいのではないかと思います（付け加えると、ここで書いていないコンポーネント、たとえば ActiveJob や ActionMailer などについては特に変わることはありません）。どちらかというと実際の開発体験としては、要素技術の変更よりは API のスキーマ情報を先に更新して gem を取り込むというワークフローが入ることが大きな変化になると思います。

ということで、gRPC のインターフェース定義から具体的に説明していきます。

### wantedly/apis - gRPC のインターフェース定義

apis はウォンテッドリーで .proto file を集約するための repository です。apis へ PR を作るだけで、CI によって「各言語向けのコード生成」が自動で行われるようになっており、「protoc のセットアップ」などの煩雑な作業を個々の開発者がしなくても良い仕組みになっています。

Ruby であれば wantedly/apis-ruby というリポジトリに生成コードがコミットされ、private gem として配信されるようになっています。 各マイクロサービスリポジトリでは、Gemfile に apis を記述した後に bundle install を行って利用するというフローになっています。 以前は apis 内のコードは明示的に require を書かなければ読み込まれなかったのですが、バージョン 2021.12.22.1128 以降から自動的に読み込まれるようになったので、現在では個々の開発者が require を書く必要はありません。

インターフェース定義におけるルール・tips などは[protobufスキーマとgRPC通信](/fields/the-system/apis)を参照してください。

### RPC の実装

さて、apis repository によって「gRPC Service Class」と「Protocol Buffers Message Class」のコードを生成して配信することができました。gRPC Server が意味のある response を返すには、これらを利用して ロジックを実装する必要があります。

一例として、`ProfileService` の `BatchGetProfiles` という RPC の処理を見てみましょう。 以下のように、apis で生成された gRPC Service Class である `W::UsersPb::ProfileService::Service` を継承した `ProfileGrpcService` Class で、 `batch_get_profiles` method を実装します。 この例は簡略化して書いてますが、実際にはメソッド内でDB への問い合わせるなどして意味のあるデータを保持したオブジェクトを作成、 最終的に `W::UsersPb::BatchGetProfilesResponse` オブジェクトとして返します。

```ruby
# Protobuf で `wantedly.foo.bar` というパッケージがあったとき、
# Ruby では `W::Foo::BarPb` というモジュールとする規約になっている
class ProfileGrpcService < W::UsersPb::ProfileService::Service
  # @param [W::UsersPb::BatchGetProfilesRequest] req
  # @param [GRPC::ActiveCall::SingleReqView] call
  # @return [W::UsersPb::BatchGetProfilesResponse]
  def batch_get_profiles(req, call)
    # ...

    W::UsersPb::BatchGetProfileResponse.new(profiles: profiles)
  end
end
```

ここで引数に渡ってくる `req` はリクエストメッセージで、 `call` は呼び出しに関する情報を持つ `GPRC::ActiveCall`のオブジェクトです (正確には `GRPC::ActiveCall::SingleReqView` または `GRPC::ActiveCall::MultiReqView`のどちらか)。`call.metadata` とするとメタデータが取得することができます。

### the\_pb と pb-serializer によるメッセージの組み立て

Ruby では通常は以下のようにして Protobuf object を作ります。

```ruby
message = W::UsersPb::Profile.new(
  user_id: profile.user_id,        # uint64
  name: profile.name,              # string
  avatar_url: profile.avatar_url,  # string
)
message.to_proto  # => binary
```

上記は最も単純な例です。[protobufスキーマとgRPC通信](/fields/the-system/apis)で紹介されているラッパー型など、 いわゆる [Well-Known Types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf) が入ってくると、もっと面倒なコードを書くことになります（面倒なのでここには書きません）。

ここで活躍するのが [the\_pb](https://github.com/wantedly/the_pb) と [pb-serializer](https://github.com/wantedly/pb-serializer) という2つの gem です。

**the\_pb** は Ruby オブジェクトを前述した Well-Known Types に変換するユーティリティを提供する gem です。 **pb-serializer** は [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) のような DSL で Protobuf object の Serializer を実装できる gem です。内部では the\_pb に依存しています。

先程例に出した `Profile` object の serializer であれば以下のように実装できます。

```ruby
# @attr_reader [::Profile] object
class ProfilePbSerializer < Pb::Serializer::Base
  message W::UsersPb::Profile

  attribute :user_id
  attribute :name
  # avatar_url が google.protobuf.StringValue だったとしても、よしなに変換してくれる
  attribute :avatar_url, allow_nil: true
end

ProfilePbSerializer.new(profile).to_pb
#  => <W::UsersPb::Profile ...>
```

the\_pb と pb-serializer をうまく活用することで、gRPC server 実装時の手間を大きく削減することが出来ます。 pb-serializer の詳しい使い方についてはリポジトリ の README や examples を参照してください。

### エラーハンドリング

Ruby における gRPC server の実装では、`GRPC::BadStatus` というエラークラスを raise することでクライアントにエラーを返すことになります。 また、エラーコードごとに `GRPC::BadStatus` のサブクラスが用意されており、それを利用することも出来ます。 以下2行は同じ意味になります。

```ruby
raise GRPC::BadStatus, Google::Rpc::Code::NOT_FOUND, "user not found"
raise GRPC::NotFound, "user not found"
```

この「エラーコード」は `google.rpc.Code` に定義されている Protobuf の Enum になります。 エラーコードの使い分けなどについては[protobufスキーマとgRPC通信](/fields/the-system/apis)を参照してください。

#### 複雑なエラー表現

さて、`GRPC::BadStatus` と `Google::Rpc::Code` を利用することでメッセージとエラーコードからなる単純なエラーを表現することは出来ました。 しかし、これでは実際のアプリケーションで表現力が足りません。 例えば単純なフォームのバリデーションエラーでも、フィールドごとにエラーメッセージを保持できる必要があります。

gRPC におけるエラーは実際には `google.rpc.Status` に定義されるような Protobuf message で表されます。

```protobuf3
message Status {
  // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
  int32 code = 1;

  // A developer-facing error message, which should be in English. Any
  // user-facing error message should be localized and sent in the
  // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
  string message = 2;

  // A list of messages that carry the error details.  There is a common set of
  // message types for APIs to use.
  repeated google.protobuf.Any details = 3;
}
```

この `google.rpc.Status` のうち、 Any の配列である `details` にエラーの詳細を詰めることが可能です。 [Google API Design Guide の Errors](https://cloud.google.com/apis/design/errors) では [google/rpc/error\_details.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto) に定義されるメッセージの利用が推奨されており、ウォンテッドリーでもそれに則っています。

Ruby でのエラー詳細の扱いに関しては、the\_pb gem で `.new_rpc_error` という関数が提供されており、それを使うと便利です。

```ruby
raise Pb.new_rpc_error(
  "post is invalid",
  code: Google::Rpc::Code::INVALID_ARGUMENT,
  details: [
    Google::Rpc::BadRequest.new(
      field_violations: [{ field: 'title', description: 'タイトルは必須です' }],
    )
  ],
)
```

### テスト

gRPC の RPC の実装は見ての通り普通のメソッドなので、最も単純には普通にメソッド呼び出しをして、返り値に対してテストをすることが出来ます。

```ruby
require 'rails_helper'

RSpec.describe ProfileGrpcService do
  let(:grpc_service) { described_class.new }
  let(:call) { instance_double("GRPC::ActiveCall::SingleReqView", metadata: {}) }

  describe '#batch_get_profiles' do
    let(:request) { W::UsersPb::BatchGetProfilesRequest.new(user_ids: user_ids) }
    let(:response) { grpc_service.batch_get_profiles(request, call) }
    let(:user_ids) do
      # ...
    end

    it 'returns user profiles' do
      # ...
    end
  end
end
```

ただ、これだと Protobuf の marshal / unmarshal 含め gRPC の内部実装を全く介さないため、 もう少し丁寧にやるなら `GrpcTesting` module[^1] (private library) の利用を検討するといいでしょう。

```ruby
require 'rails_helper'

RSpec.describe ProfileGrpcService do
  include GrpcTesting
  let(:stub) { test_stub_class(described_class.superclass.module_parent).new(described_class.new) }

  describe '#batch_get_profiles' do
    let(:response) { stub.batch_get_profiles(request, call) }

    # ...
  end
end
```

### 起動・デバッグ

次に、gRPC Server Process を起動します。gRPC Server を起動して Request を処理させるには、`app/grpc_services` に定義した「gRPC Service Class を継承した Class」を Handler として登録する必要があります。また、ウォンテッドリーでは、マイクロサービスは Kubernetes 上で Docker Container として動かしているので、Heath Check の為に gRPC Health Checking Protocol を実装した Handler の登録も必要です。その他、Observability を高めるための gRPC interceptor（gRPC の拡張機能。後述するが、Rack Middleware 相当のもの）の設定なども行う必要があります。

こういった「どの repository でも共通で必要になる処理」については、個々の開発者が気にする必要がないよう、[servicex (internal)](https://github.com/wantedly/servicex-ruby) と呼ばれる社内 library (gem) の機能として提供するようにしています。具体的には、grpc-server という名前のコマンドを用意しており、それを実行するだけで「gRPC Service Class の load および Handler としての登録、その他に共通で必要になる各種設定」が行われるようにしています。

`grpc-server` コマンドは以下のように利用することができます。

```
$ bundle exec grpc-server
handling /grpc.health.v1.Health/Check with #<Method: Grpc::Health::Checker#check>
handling /grpc.health.v1.Health/Watch with #<Method: Grpc::Health::Checker(Grpc::Health::V1::Health::Service)#watch>
handling /wantedly.users.UserService/ListUsers with #<Method: UsersGrpcService#list_users>
.
.
.
gRPC server starting...
* Listening on tcp://0.0.0.0:6046
* Environment: development
Use Ctrl-C to stop
```

この `grpc-server` コマンドのうち、Health Checker やシグナルハンドリングなどの設定を隠蔽したものは [grpc\_server](https://github.com/wantedly/grpc_server) gem として公開されています。

#### gRPC server にリクエストを送ってデバッグする

JSON/HTTP な API であれば cURL コマンドや [Insomnia](https://insomnia.rest/), [Postman](https://www.postman.com/) といったツールで実際にリクエストを送ってデバッグすることがあるかもしれません。 gRPC server では[標準でコマンドラインツールが提供されている](https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md) ほか、[grpcurl](https://github.com/fullstorydev/grpcurl) や [evans](https://github.com/ktr0731/evans) といった 3rd party のツールが存在します。 一方でこれらのツールでは「元になった .proto ファイル」もしくは「Server Reflection に対応した gRPC server」が必要になり、ウォンテッドリーの環境では少し使いづらいという問題がありました。

そこでウォンテッドリーでは [giro](https://github.com/rerost/giro) というツールを利用し、なるべく少ない手順・依存で gRPC server にリクエストを送れるような環境になっています。詳しくは [giro の解説をしているブログ](https://www.wantedly.com/companies/wantedly/post_articles/310167) もしくは [giro の使い方ドキュメント (internal)](https://github.com/wantedly/apis-reflection-server) を参照してください。

## Ruby で gRPC Client を使う

gRPC Client を開発する際にも、gRPC Server 開発と同様に .proto file を集約する apis repository や servicex と呼ばれる社内 library (gem) を活用するようにしています。

gRPC Client としては、apis repository で自動生成した gRPC Service Class のコードを load したうえで、その Stub Class を利用します。gRPC Client を単純に利用するだけであれば、以下のコードで動きます。

```ruby
client = W::UsersPb::ProfileService::Stub.new(url, :this_channel_is_insecure)
```

ただし、Production の Microservices で利用する上では「必ずセットして欲しい gRPC interceptor（gRPC の拡張機能。後述するが、Faraday Middleware 相当のもの）」などが存在するので、個々の開発者が意識しなくてもそれらの設定が自動で行われる様に、servicex gem の中で `.stub_for` という「gRPC Client 生成用のメソッド」を用意しています。

```ruby
client = Servicex::Grpc.stub_for(W::UsersPb::ProfileService, grpc_url)
```

`.stub_for` の実装はだいたい以下のようになっています。 元々、HTTP/1.1 の通信では servicex gem の中で Faraday Middleware などの設定を行なった API Client を提供する様にしていました。 `.stub_for` は同様の体験を gRPC でも提供することが意図されています。 Observability を確保するための interceptor のセットや User Agent の設定なども `.stub_for` で自動で行われるようになっています。

```ruby
# @example
#    Servicex::Grpc.stub_for(W::UsersPb::ProfileService, UsersApi.base_url)
# @param service_class [Class<Grpc::GeneralService>]
# @param url [String]
# @yield [opts] To modify options for instantiating a Stub
# @yieldparam opts [Hash] Default options
def stub_for(service_class, url)
  opts = {
    channel_args: {
      'grpc.primary_user_agent' => Servicex.user_agent,
    },
    interceptors: Servicex::Grpc.client_interceptors,
  }
  yield opts if block_given?
  service_class::Stub.new(url, :this_channel_is_insecure, opts)
end
```

gRPC Client については、基本的にやる事はこれだけです。 「apis と servicex を gem として load すれば、どのマイクロサービスからでも簡単に必要な設定が行われた状態で gRPC での通信が出来る」という環境を作っています。

最終的にはだいたい以下のようなコードを書くことになります（次節で説明するエラーハンドリングをしない場合のパターンです）。

```ruby
client = Servicex::Grpc.stub_for(
  W::Users::ProfileService,
  UsersGrpcApi.base_url,  # servicex が提供する、ほかのマイクロサービスの URL を取得する関数
  current_user_id: current_user.id,
)
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)

data = client.batch_get_profiles(req)
data  # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>
```

### gRPC Client のエラーハンドリング

gRPC における通信の失敗は gRPC のエラーコードとして表されます。 ([Handbook - gRPC 通信のエラーハンドリング](https://docs.wantedly.dev/fields/system/apis#grpc-noerhandoringu) にその種類と、デフォルトで考慮すべきものがどれかの指針が書かれているので、確認しておいてください。)

マイクロサービス・アーキテクチャでの開発を行う際には、障害の分離のために、通信の失敗について考慮する必要があります。 通信が失敗した際、全体を失敗させるのではなくなんらか部分的に処理が継続可能な場合は、メソッド呼び出しで errors に想定しているエラーの種類を列挙してください。

これは、

```ruby
client = Servicex::Grpc.stub_for(
  W::Users::ProfileService,
  UsersGrpcApi.base_url,
  current_user_id: current_user.id,
)
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)

res = client.batch_get_profiles(req, errors: [:deadline_exceeded])
```

これは、GRPC のエラーの種類に対応するシンボルとして指定できます。例えば、GRPC のエラーである DEADLINE\_EXCEEDED に対応するシンボルは `:deadline_exceeded` です。 また、サーバー側に原因があると考えられる UNKNOWN, INTERNAL, UNAVAILABLE を `:server_errors` でまとめて指定できるようにしています。

多くのケースでは、`errors: [:server_error, :deadline_exceeded]` を書くことをまず考えるのが良いプラクティスでしょう。コードレビューの際にも、そのことをチェックしてください。

errors を指定すると成功と失敗を表現するオブジェクトが返ってくるので、パターンマッチで処理しましょう。

```ruby
res = client.batch_get_profiles(req, errors: [:server_errors, :deadline_exceeded])
case resp
in Result::Success(data)
  data # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>
in Result::Failure(error)
  # Failure の場合は自動で Honeybadger に送られます。
  error # => [<W::Users::ProfileServiceError: ...>]
end
```

エラーの種類によって個別の処理をしたい場合は、こういう風にパターンマッチを書くと良いでしょう。

```ruby
res = client.update_profile(req, errors: [:server_errors, :deadline_exceeded, :permission_denied])
case resp
in Result::Success(data)
  data
in Result::Failure(type: :permission_denied, error: _error)
  Pb::Visit::ToastError.new(message: "このデータを更新する権限がありません")
in Result::Failure(_error)
  Pb::Visit::ToastError.new(message: "更新が行なえませんでした。しばらくしてからもう一度やり直してください。")
end
```

### gRPC Client のモック

多くの場合、テストコード中ではマイクロサービス間通信を含む外部へのリクエストを禁止したくなります。 HTTP であれば [webmock](https://github.com/bblimke/webmock) gem を使うことが多いですが、gRPC では [grpc\_mock](https://github.com/ganmacs/grpc_mock) という gem を利用しています。

```ruby
before do
  GrpcMock.stub_request('/wantedly.users.ProfileService/BatchGetProfiles').to_return do |req, call|
    W::UsersPb::BatchGetProfilesResponse.new(
      profiles: [],
    )
  end
end
```

## ライブラリ・ツールの紹介

Ruby で gRPC を利用するときに利用する内製の gem やツールの一覧です。 pb-serializer や giro のように直接的に利用するものもあれば、servicex 内で利用されているものもあります。

* [the\_pb](https://github.com/wantedly/the_pb)
  * Ruby で Protocol Buffers を扱う際に便利なユーティリティを集めた gem
* [pb-serializer](https://github.com/wantedly/pb-serializer)
  * ActiveModelSerializer ライクな DSL で Ruby オブジェクトを Protobuf に変換することができる
* [computed\_model](https://github.com/wantedly/computed_model)
  * pb-serializer と組み合わせることで、データソースを抽象化したデータの読み込みや GraphQL ライクな field selector が実装できる
* Observability を支える interceptor たち
  * [grpc\_newrelic\_interceptor](https://github.com/wantedly/grpc_newrelic_interceptor)
  * [grpc\_opencensus\_interceptor](https://github.com/wantedly/grpc_opencensus_interceptor)
  * [grpc\_access\_logging\_interceptor](https://github.com/wantedly/grpc_access_logging_interceptor)
* [reloader\_interceptor](https://github.com/wantedly/reloader_interceptor)
  * gRPC server 開発で Rails のような hot-reloading を実現する
* [grpc\_typechecker: A dynamic type checker for gRPC methods](https://github.com/wantedly/grpc_typechecker)
  * RPC が返しているオブジェクトが正しい Protobuf messaage かどうかを検証する interceptor
  * see also [gRPC Ruby でハマらないための型チェッカー | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/298998)
* [grpc\_server](https://github.com/wantedly/grpc_server)
  * gRPC server を起動するためのボイラプレートを隠蔽した gem
* [rerost/giro](https://github.com/rerost/giro)
  * 簡単に gRPC server を叩ける CLI
  * [Server Reflectionが実装されていないgRPCサーバーでも簡単に叩けるCLIを作る | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/310167)

#### 話を聞きに行きたい

* Slack: [#rubyist](https://wantedly.slack.com/archives/CB734C068)

#### もっと知りたい

* Wantdly の Rails のアーキテクチャについて理解したい
  * [マイクロサービス・アーキテクチャと共存する Ruby on Rails のアーキテクチャ的拡張 - その事例と可能性 / The Architectural Extension of Ruby on Rails to fit to microservices - Speaker Deck](https://speakerdeck.com/altech/the-architectural-extension-of-ruby-on-rails-to-fit-to-microservices)
* gRPC について知りたい
  * [Real World Performance of gRPC - gRPC 利用による劇的なパフォーマンス改善 | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/220495)
  * [gRPC Internal - gRPC の設計と内部実装から見えてくる世界 | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/219429)

[^1]: <https://github.com/wantedly/users-rails/blob/master/spec/support/grpc\\_testing.rb>


# 実践: gRPC in Go

本章では、 「ウォンテッドリーで Go を使って gRPC Server/Client の開発をどう行なっているのか」について紹介します。

## Go で gRPC Server を作る

ウォンテッドリーでは [grapi](https://github.com/izumin5210/grapi) という小さなフレームワークを使って gRPC server を実装します。 詳しくは下記のスライドおよび記事を参照してください。

* [grapi: Bulding JSON API server with grpc-gateway for microservices - Speaker Deck](https://speakerdeck.com/izumin5210/grapi-bulding-json-api-server-with-grpc-gateway-for-microservices)
* [grapi : #golang で interface driven かつボイラプレートに悩まされない API 開発](https://qiita.com/izumin5210/items/ed1c0100a45366c3b75e)

grapi が生成したコードに servicex の設定を組み込む必要があります。これは [servicex-go の README (internal)](https://github.com/wantedly/servicex-go) を参照してください。

## Go で gRPC Client を使う

基本的には [gRPC 公式の Quick start](https://grpc.io/docs/languages/go/quickstart/) と同じになります。 ただ、Ruby と同じくクライアントの初期化については servicex の実装を使う必要があります。

```golang
cc, err := grpcx.DialContext(ctx, apix.UsersGrpcBaseURLs.Default().String())
if err != nil {
	// ...
}
client := users_pb.NewProfileServiceClient(cc)
```

#### 話を聞きに行きたい

* Slack: [#gophers](https://wantedly.slack.com/archives/CA69S8BBP)


# GraphQL Gateway - アプリ向けに API を公開する

ウォンテッドリーでは、システム内部のマイクロサービス間通信に Protocol Buffers / gRPC を利用しています（『[protobufスキーマとgRPC通信](https://docs.wantedly.dev/fields/the-system/apis)』の章）。

では、他のマイクロサービスではなく、Webアプリやモバイルアプリに向けて API を提供する場合についてはどうすると良いでしょうか？

この章では、アプリから使えるシステムの API（まさに "Application Programming Interface" です）を用意する際に使う、GraphQL Gateway について概説します。

## GraphQL Gateway とは？

GraphQL Gateway とは、システムの中に数あるマイクロサービスの一つで、アプリ向けに GraphQL API を提供するものです。

基本的に、アプリからシステムに対しての API 呼び出しは全て GraphQL Gateway が引き受けることを想定しています（まだこのアーキテクチャに移行していない機能は多いので、それらは直接各マイクロサービスの REST API を叩いています）。したがって、システムの中でも前段に立つコンポーネントであると言えるでしょう（「[技術とアーキテクチャ](https://docs.wantedly.dev/introduction/technical-overview)」の章の図も併せてご参照ください）。

理想状態として、この GraphQL Gateway はシステムが持つドメインオブジェクトとそれに対する操作を全て提供します。従って、アプリから見ると、システムのインターフェースは GraphQL になっており、そこでできることは GraphQL スキーマを見ることによって探索できる状態になります（実際には、GUI である [GraphQL Voyager](https://graphql-gateway-qa.internal.wantedly.com/voyager)（内部向けリンク）を利用することになるでしょう）。そして、スキーマを見ながら、アプリは自ら欲しいデータをクエリによって選択的に取得します。

動作としては、GraphQL Gateway は受けたリクエスト内容をもとに後段のマイクロサービスに対して gRPC の呼び出しを行います。この結果を集約し、オブジェクトグラフを構築してレスポンスとして返却します。後述するように、この作業はフレームワーク化されているため、全てをマニュアルで記述する必要はありません。

## なぜ GraphQL なのか?

マイクロサービス間通信に Protocol Buffers / gRPC を使っていながら、なぜアプリとの通信では GraphQL を利用するのか。この「目的」の部分を理解することが、GraphQL で実際にスキーマ設計を行う際などにも非常に重要となります。

それは一言で言えば、ビジュアルデザインやレイアウトデザインに依存しない、**情報設計**の抽象度で API を提供することで、全体の **ソフトウェアデリバリのパフォーマンスを最大化する** ためです。

### どのような問題を解決できるか

具体的に見ていきましょう。技術とアーキテクチャの章の図からもわかるように、Wantedly には複数のアプリが存在します。

Visit サービスに関して言うと、ツー・サイド・マーケットプレイスのサービスのため、個人ユーザーと企業ユーザーの2種類が存在します。これらはそれぞれ別の Web アプリを利用します。また、個人ユーザー向けには Web アプリと同じ機能（あるいはそのサブセット）を iOS アプリと Android アプリにも提供しています。つまり、マルチ・プラットフォーム展開をしています。

サービス特性上、これら複数あるアプリに同じコンテンツが表示（あるいは編集）されます。例えば、個人ユーザーが作成した「プロフィール」や、企業ユーザーが作成した「募集」などです。

つまり、情報としては同じ情報がさまざまなアプリに出ることになります。ただ、画面の大きさ（モバイルかどうか）や個人ユーザー向けか企業ユーザー向けか、などによってレイアウトや情報量は変わります。

こういった、いろいろあるアプリごとに API を作っていては、その度にバックエンドとフロントエンドでのコミュニケーションが発生してしまい、ソフトウェアデリバリのパフォーマンスが下がってしまいます。

#### 新機能を追加する例

一つの理想として言うならば、ある機能を新たに追加する際、次の図のようにシステムの実装は最初の一度きりであるべきでしょう。例えば Visit に対して「副業意欲・転職意欲」を表示・入力できるようにするとします。これの開発のタイムラインを単純化して図示すると、次のようになります：

![](/files/GxVpSkvUKApfeWcuPHuk)

ここで見落としがちなのは、アプリのエンジニアだけで実装できることで、「実装期間の短縮」「コミュニケーションコスト」だけではなく、バックエンドも含めた「リソースの調整」が不要となる点です。こういったリソース・アロケーションの疎結合化が、組織全体のソフトウェアデリバリのパフォーマンスを押し上げます。

これを実現するためにシステムの設計・実装は、“いま目の前にあるアプリ”のことだけでなく、実装しようとしているオブジェクトが“将来的に別のアプリや別の画面で使われるときのこと”も想定する必要があるので留意しましょう。

#### グロースを行う例

別の例として、こういった機能のゼロ→イチのフェーズだけでなく、グロースのフェーズでも GraphQL によって情報設計のレベルで分離することはソフトウェアデリバリの速度を向上させます。

例えば、コロナ禍にあってカジュアル面談のオンライン化のニーズが増えた結果、募集の一覧画面に「オンライン面談できるよ！」というタグを出す A/B テストをするとしましょう。この場合、「募集」という GraphQL オブジェクトのフィールドとして「オンライン面談できるかどうか」があるでしょうから、Web アプリの GraphQL クエリを少し編集するだけで実装ができるため、実装からリリースまで1日以内に行うことが可能になるでしょう。

![](/files/taZ9640Sslu8060W5HCW)

以上は、アプリのソフトウェアデリバリに着目した例ですが、技術領域を跨いだコミュニケーション回数の削減は当然バックエンドの負担も減らすため、バックエンドのシステム改善を加速させることが可能となります。

### どのように問題を解決するか

技術的になぜ GraphQL が前述のような問題を解決できるかというと、「スキーマ」や「クエリ言語」といった仕組みを仕様に取り込むことで可能になっています。

![](/files/QT257XkkhQmqdt7etw0u)

これは責務の観点から説明すると、「どの範囲のデータをクライアントが必要とするか」の詳細をサーバーサイドではなく、クライアントサイドがより決められるようになっている、ということでもあります。

付随的に、「スキーマ」「クエリ言語」の仕組みがあることで、サーバーサイド・クライアントサイドにおける型定義の生成などの開発者体験の向上が可能になっています（これ自体は、Protocol Buffers からも open API 経由で似たようなことはできますが）。

## GraphQL Gateway の開発フレームワーク

上記のようなことを実現するための GraphQL Gateway のフレームワークについて概説します。

基本的に、GraphQL Gateway は GraphQL Helix という GraphQL サーバー上で Nexus というスキーマ構築ライブラリを利用して記述します。ただし、背後のマイクロサービスとの繋ぎ込みを簡易にするため、Protocol Buffers のスキーマを活用しているという特徴があります。それをここで説明します。

概念的には、GraphQL スキーマはオブジェクトグラフとそれに対するエントリポイントに分けて考えられます。さらに、オブジェクトグラフの方は、ノードに相当する「オブジェクト」とエッジに相当する「リレーション」があります。そして、エントリポイントは、クエリとミューテーションの二種類が存在します。したがってスキーマとして定義すべき要素は次のようになります。

* グラフ自体…オブジェクト、リレーション
* グラフに対するエントリポイント…クエリ、ミューテーション

このうち、一番基礎となるのがオブジェクトですが、これは [wantedly/apis (internal)](https://github.com/wantedly/apis) で定義した Protocol Buffers のメッセージ定義から（コメントなどのメタデータも含めて）自動的に生成されます。なので、GraphQL スキーマを定義する順序としては、

1. オブジェクトの定義を Protocol Buffers 上できちんと固めて apis にマージする
2. それを参照しつつリレーション、クエリ、ミューテーションがあれば GraphQL Gateway 上で定義する（実装もここで行う）

の2ステップとなります。なお、「あれば」と書いたのは、例えば既存のオブジェクトに新しくフィールドを追加するだけのことも多いためです。この場合は、Protocol Buffers からのオブジェクト生成を再実行したコードをマージするだけになります。

定義の流れとしては以上のようになります。実装としては、リゾルバ（GraphQLオブジェクトをどのように取得するかのロジック）を書くことになり、そこに gRPC の呼び出しコードを書く(gRPC の RPC はステップ1で一緒に定義してしまうことが多い)という形になります。次のコードは、`projects` というクエリの定義と、対応するリゾルバの実装になります。

```typescript
// src/schema/projects/projects.ts

export const projects = queryField("projects", {
  type: nonNull(list(nonNull("Project"))),
  description: "募集一覧に表示する募集の一覧",
  async resolve(_root, _args, ctx, _info) {
    const req = new ListProjectsRequest();
    const resp = await ctx.dataSources.projects.project.promises.listProjects(req, ctx.getGrpcMetadata(info)); // 新しい gRPC サービスが増える場合は、別途 dataSources のマッピングを更新
    return resp.getProjectsList();
  },
});
```

このように GraphQL Gateway はデータの集約（アグリゲーション）を行いますが、ドメイン知識 (ドメインロジック, ビジネスロジック) は一切持たないので注意しましょう。

以上は概説になります。より実践的なハンズオンは[リポジトリ配下のドキュメント](https://github.com/wantedly/wantedly-graphql-gateway/tree/master/docs)にあるので、コードを書く際はそちらを参照してください。

## スキーマ設計について

最後に、GraphQL の **最も重要な部分はスキーマ設計** になります。

冒頭、GraphQLを入れる理由としてソフトウェアデリバリの全体的な向上を挙げましたが、それを達成できるかどうかは良いスキーマを設計できるかどうかに掛かっていると言えます。

そして、いろいろなアプリで使われる API ということは、一度出してしまったスキーマはなかなか変更できない、ということも同時に意味します（全く変更できない訳ではありませんが、コストが高いという意味です）。

スキーマ設計は非常に重要なため、別途の章を設ける予定です。ただ、GraphQL スキーマについて一般的なことは、『[Production Ready GraphQL](https://book.productionreadygraphql.com/)』という本の前半部分にまとまっているので、スキーマの設計を多くやる場合は、目を通しておくことをお勧めします。

#### 話を聞きに行きたい

* Slack: [#backend\_chapter](https://wantedly.slack.com/archives/C01GY4RF5BK)

#### もっと知りたい

* [GraphQL Gateway docs](https://github.com/wantedly/wantedly-graphql-gateway/tree/master/docs) (internal)
* [みんなで育てる GraphQL スキーマ, それを支える Protobuf - Speaker Deck](https://speakerdeck.com/izumin5210/graphql-and-protobuf-number-tech-stand)
* [技術を的に当てる技術について - GraphQL を入れ直した話 - Speaker Deck](https://speakerdeck.com/altech/re-introduce-graphql)


# Wantedly Visit で BFF GraphQL サーバーを辞めた理由

## 概要

ウォンテッドリーでは2019年頃から(Web・モバイル)アプリとシステムの通信に GraphQL を使用しています。この章では GraphQL 導入に至った背景とこれまでの歴史について紹介します。

紹介する内容

* Backend For Frontend (BFF) として導入された GraphQL サーバー
* 我々の GraphQL サーバーは BFF ではなく API Gateway であるという解釈変更
* 解釈変更に伴って生まれた新しい GraphQL サーバー

## Backend For Frontend (BFF) として導入された GraphQL サーバー

最初の GraphQL サーバーがウォンテッドリーに登場したのは 2019 年初頭のことです。募集作成画面という機能をリアーキテクチャ含めたリニューアルをするタイミングで導入されました。このリアーキテクチャでは主に以下のことを実施しました。

* フロントエンドの刷新。Ruby on Rails で書かれていた UI を、独立した React 製のアプリケーションへと移行する。
* バックエンドの刷新。募集作成に関する機能をウォンテッドリーのマザー Rails から切り離し、マイクロサービスへと移行する。
* フロントエンドとバックエンドとの通信のために、GraphQL サーバーを導入する。

老朽化しメンテナンス性の低くなっていたフロントエンドのコードベースを、独立したリポジトリに移行することで改善サイクルを回しやすくするという目的がありました。またバックエンドに関しては People の方で成功していたマイクロサービス化を Visit の方にも取り入れメンテナンス性の向上などを図ろうと考えていました。 システムがマイクロサービス化していくとフロントエンド側から見ると、取得したいリソースごとにリクエストするサービスを変えなくてはいけないというデメリットが生じます。あるリソースは JSON/HTTP を使ってサービスAにリクエストするが、別のリソースは ProtoBuf/HTTP を使ってサービスBにリクエストしないといけない、といった具合です。 こういったデメリットを解消するためアプリとシステムの間に GraphQL サーバーを置くという判断になりました。GraphQL がシステムの入り口にいると、GraphQL のクエリをインターフェースとして後ろにいるシステムが抽象化されます。

募集作成のリニューアルのあとも、別の機能のリニューアル・マイクロサービス化を含むリアーキテクチャが行われる予定がありました。これから増えていくマイクロサービスのリクエストの口として効果を期待されて導入されました。

導入初期はあくまでも募集作成画面を含む採用管理画面のための GraphQL サーバーという立ち位置であり、フロントエンドのためにシステムとの通信を肩代わりする役割として存在していたため Backend For Frontend (BFF) として認識されていました。

## BFF ではなく API Gateway であるという解釈変更

最初の GraphQL サーバーを数年運用しているといくつか問題が見えてきました。

* システムの Protobuf 定義と GraphQL スキーマ管理による二重のスキーマ定義
* GraphQL と Protobuf の値の不毛な変換処理

### システムの Protobuf 定義と GraphQL スキーマ管理による二重のスキーマ定義

ウォンテッドリーでは各マイクロサービスは基本的に gRPC を採用しており、Protobuf を使って通信を行っています。.proto ファイルで rpc 定義が書かれており、これは中央集権的に管理されています。各機能の開発するときにはまず .proto ファイルに rpc や message 定義を記述します。この .proto ファイルから protoc を使って生成したコードを使ってクライアントを開発します。 GraphQL サーバーを開発するときもまずはスキーマを定義します。ウォンテッドリーでは nexus を使ってコードファーストにスキーマを定義しています。お気づきのように、ここでスキーマの二重定義が発生してしまっています。.proto ファイルでシステムの rpc を定義し、それを GraphQL サーバーから呼び出せるように GraphQL スキーマに翻訳する必要があるのです。 システム側での定義が完了したと思ったら、アプリから呼び出すためには GraphQL の方でも同じような作業をしないといけない状況で、不要な開発コストが発生してしまっていました。

### GraphQL と Protobuf の値の不毛な変換処理

さらに開発上で煩雑だったポイントとして、GraphQL の世界と Protobuf の世界の間での不毛な値の変換が発生してしまっていたことがあります。特に Enum 周りの変換が不毛で `gqlToProtoFoo` や `protoToGqlFoo` といった関数が大量に発生してしまいました。

### 解釈変更

もともと Backend For Frontend として生まれた GraphQL サーバーですが、実際のところ、このレイヤーでフロントエンドアプリケーションのための処理というのは行われていませんでした。なるべくドメインロジックを載せないようにという方針も相まって、最初に生まれた GraphQL サーバーは実態としては API Gateway でした。 また、もともとウォンテッドリーでは Web で成功した機能をモバイルアプリに展開するというのがよくある流れでした。このときに、Web で使っていた WebAPI をそのまま流用するのではなく、モバイルアプリ用に追加実装するということが起きていました。しかし理想的には一つの機能については一つの WebAPI のみが提供されているべきです。 cf. <https://docs.wantedly.dev/fields/the-system/graphql-gateway#wosuru> 上記背景から我々の GraphQL サーバーは BFF として存在するのではなく、API Gateway として存在するべきだという解釈に変更になりました。

## 解釈変更に伴って生まれた新しい GraphQL サーバー

解釈変更に伴い、新しい GraphQL サーバーが立ち上がりました。これは前の GraphQL サーバーが抱えていた課題を解決するために生まれたものです。

* `.proto` ファイルから nexus の DSL を生成し、スキーマの二重定義と不毛な値の変換処理記述を解消

新しい GraphQL サーバーでは [proto-graphql-js](https://github.com/proto-graphql/proto-graphql-js) というツールを使って、`.proto` ファイルから nexus の DSL を生成しています。ここで生成された DSL をベースに、さらにスキーマ定義を拡張したり、DataLoader を実装することで、一つの GraphQL スキーマとして動作するようになっています。 ここに関しての説明は[GraphQL Gateway の開発フレームワーク](https://docs.wantedly.dev/fields/the-system/graphql-gateway#graphql-gateway-nofurmuwku)に詳しく書いてあります。


# 実践: GraphQL スキーマ設計(未執筆)


# API での認可処理 (Authorization)

誰がどのように、システムが持つリソースを取得・変更して良いのかの権限チェックを認可処理といい、システム上では何らかの認可処理が必要となります。

認可処理について、チームによって異なる方法を取っていたり、共通の認識がなかったりすると、チェックの漏れや重複が発生し問題となります。本章では、マイクロサービスアーキテクチャを前提とした、単純な認可処理の方針を定めています。

## TL;DR

* 認可処理はどこで行うか? → 該当データのオーナーとなるマイクロサービスの API を境界として、API の実装に認可処理を含める。
* 認可処理はいつ必要があるか? → デフォルトでは行うが、行わないことを API 単位で表明して認可処理をスキップすることもできる。

## 前提となる枠組み

### APIエンドポイントの分類

全ての API のエンドポイントは、以下の二つのいずれかに分類できます。

> **exposable**…呼び出し元のユーザーに基づいたデータの取得・変更の権限チェックが行われているもの（デフォルト）
>
> **unexposable**…そのような制御が行われておらず、任意のデータの取得・変更が行えるもの ... internal

exposable な API は暗黙的に「呼び出し元のユーザー」という入力をインターフェースに持ちます。例えば、プロフィール情報の取得 API であれば、つながりなどの公開範囲設定に応じて返すプロフィール情報が変わるように実装します。

unexposable な API は日時のバッチ処理からの呼び出し（システム内部での処理）や管理画面など、入力内容も含めて API 呼び出し全体が信頼できると仮定できる前提で用意するものです。

これらの分類は、直接的な認可処理に限らず、API の責務全体を定義します。例えば、ActiveRecord で生成される updated\_at はシステムの内部情報なので、仕様として公開することを意図していない限り exposable な API で返すべきではありません。

デフォルトでは exposable なものとしてエンドポイントを分類・実装するようにします。これは「各マイクロサービスが自分自身の持つデータについて取得・変更の権限を確認するようにする（別のサービスに任せない）」ということでもあります。

一方で unexposable なものとして実装されたエンドポイントは、basic authentication（!= authorization）やネットワーク的に隠すなど、内部の者で信頼できることを示す何らかの証拠を求めるべきです。

### システム全体の動作

我々の理想とするアーキテクチャにおいて、エンドユーザーからのAPI呼び出しはまず GraphQL gateway が引き受けます。言い換えれば、それ以外の API（システム API と呼ぶことにする）は全て隠蔽されていて、エンドユーザーが直接叩くことができません。

この gateway は、cookie session や token などのクライアント情報を元に「呼び出し元のユーザー」を認証します。そして、システム API を認証済みの「呼び出し元のユーザー」を入力として叩きます。

システム API が他のシステム API を叩く場合は、バケツリレー方式でこの認証情報を伝搬し、同じインターフェースで入力に含めます。

通常は、**gateway が引き受ける API 呼び出しから間接的に呼び出される API は全て exposable なものとなる** はずです。

## 規約

以上のような枠組みをサポートする、いくつかの規約を導入します。

### 規約1：API の種別の表明

API の種別を proto で次のように表明するようにします。`skip_authorization = true` であれば unexposable、`skip_authorization = false`　であれば exposable、何も書かなければゼロ値で `false` なのでやはり exposable、となります。つまり、デフォルトは exposable とすることを意図しています。

```protobuf3
rpc Echo(SimpleMessage) returns (SimpleMEssage) {
  option (wantedly.api.rpc).skip_authorization = true;
}
```

API を実装するコードレビューの観点では、`skip_authorization = true` という表明がない限り、呼び出し元のユーザーに応じて適切な取得・変更の制御を行なっていることをチェックすべきです。

API を呼び出すコードレビューの観点では、その呼び出し元の API 自体が信頼できる呼び出し元から呼び出されているのでなければ、`skip_authorization = true` の表明をしている API につなぎこまないようにチェックすべきです。

### 規約2：「呼び出し元ユーザー」の入力インターフェース

「呼び出し元ユーザー」の入力は、`x-current-user-id` という名前の HTTP / gRPC ヘッダーに入れるようにします。"current user id" よりは "authenticated user id" の方が本来の意味と近いですが、既存のコードを鑑みてこのようにしています。

社内マイクロサービス共通ライブラリである servicex にも特別のインターフェースを用意して、バケツリレーが必要な場合や、信頼できる呼び出し元からの実行を助けていくのが今後の方針です。実際に規約をどう実現するかに関しては、言語・F/Wごとにデザインすることになるので本章では触れません。

例：

```ruby
YashimaApi.new(current_user_id: current_user.id).get(...)
```

### 規約3：認可に失敗した場合の表明

以下のステータスコードを返すようにします。

* HTTP: 403(Forbidden)
* gRPC: PERMISSION\_DENIED

### 補足: 規約に含めないと判断した項目

以下は将来的に組織がスケールしたりプロダクトが成長した場合に導入される可能性があるものです。現時点ではコストに見合わないと判断しています。

* 「スコープ」のような高級な抽象化 ... スコープのリストと意味自体をアプリケーションとシステムで意識する必要が出てくる

以下は案として出たが期待する問題をうまく解決してくれるかわからないのでひとまずやらないとなったものです。

* 「会社」を認証対象にする（current-company-id のようなもの）... 複数の認証対象を取り扱う必要が出てくる

以下はやらない方が今後も良いだろうと判断しているものです。

* current-user-id のような認証情報を暗黙的に伝播すること ... 例えば Sidekiq のようなバックグラウンドジョブに渡すときには、常に明示的に渡しているが困っていないため

## 規約の適用・実装に関して

方針と規約はアーキテクチャ上の決定として用意しますが、各言語・コーディングレベルでどのようにその規約を適用するかはある程度自由に考えることとします。

例えば、ある言語・フレームワークでは、unexposable な API を特定の名前空間にまとめる、ということが自然である場合はそのようにするのが良いです。実際に、wantedly/wantedly の gRPC services 以下は、そのような実装になっています。

#### 話を聞きに行きたい

* Slack: [#backend\_chapter](https://wantedly.slack.com/archives/C01GY4RF5BK), [#microservices](https://wantedly.slack.com/archives/CARTLEXR9)

#### もっと知りたい

* [ユーザーのデータアクセス権限の認可をどうしていくべきか · Issue #431 · wantedly/dev (internal)](https://github.com/wantedly/dev/issues/431)
* [システムが持つデータの取得・変更の権限チェックの仕方（認可：Authorization） · Issue #487 · wantedly/dev (internal)](https://github.com/wantedly/dev/issues/487)


# マイクロサービス共通ライブラリ "servicex" の紹介

## TL;DR

* 共通ライブラリを導入することで
  * 「**すべてのマイクロサービスが備えるべき機能を間違いなく提供できる**」だけでなく
  * 「**アーキテクチャ全体を継続的に・小さく改善していく足がかり**」にもなった
* 各言語に実装した共通ライブラリのコア機能は「すべてのサービスが」「すべての入出力にフックし」「入力のコンテキストを出力まで伝搬する」であった

## マイクロサービス共通ライブラリ"servicex"

ウォンテッドリーではマイクロサービスアーキテクチャを採用しています。[Wantedly における Go 導入にまつわる技術背景](https://www.wantedly.com/companies/wantedly/post_articles/193633) などでも言及のあるとおり、Ruby・Go・Python・Node.js（・Rust）と複数の言語が採用されています。 そして、"**servicex**" という共通ライブラリがこれらの言語それぞれに用意されています。 servicex の各言語実装はすべて同じ目的を達成するためのもので、その Why / What は以下のように定義されています。

> ## Why
>
> 新しくサービスを作る際に考えることを減らし，本来実現したいドメインに集中できるようにしたい．
>
> ## What
>
> ウォンテッドリーのすべてのマイクロサービスが備えるべき機能を扱いやすい・統一的な形で提供する．

(wantedly/servicex の README より抜粋)

本章ではこのマイクロサービス共通ライブラリ "servicex" について、なぜ必要だったのか、そしてそれがあることでどんな良いことがあったかなどを紹介します。

### 何をしてくれているか

機能ベースでは、大きく2種類に分けられます。

まず、ログやメトリクスなど Observability 関連。 マイクロサービスアーキテクチャを採用するとリクエストの流れも複雑になりますし、障害発生時の原因特定などの難易度も上昇します。 よって、それぞれのコンポーネントで Metrics, Logs, Traces などを収集しておくことは重要なのです。 ウォンテッドリーのマイクロサービスでは、開発者が意識せずとも servicex さえ導入しておけば、これらの機能が有効になります。

また、だいたいのマイクロサービスで必要になる便利機能群というのも実装されています。 いくつか挙げてみましょう。

* マイクロサービス間通信のデフォルトタイムアウト・認証などの設定
* マイクロサービス間通信で、環境（ローカル, 開発, QA, 本番, ..）を見て URL をいい感じにしてくれる
* BigQuery にイベントを記録できる機能
* RDB に依存しない A/B テスト機構
* ...

ほかにもいくつか機能がありますが、ここでは省略します。

### なぜ必要か

「（プロダクト開発者としては）設定を忘れがちだが、忘れたらかなり困る」ものを忘れないようにするためです。特にログやメトリクスはわかりやすいでしょう。 普段からメトリクスが取れてないと、問題発生時に気づくのが遅れるかもしれません。 また、アクセスログなどは問題に調査・対応するときの最後の砦となることもあります。 こういうものはだいたい問題が起きたタイミングで初めて需要が生まれます。しかし、その時点でログが取れてないことに気づいても後の祭りです。

それに、ログやメトリクスの送り先にもちゃんとルールが必要です。 マイクロサービスによってログテーブルの命名規則やデータフォーマットが違っていたらちょっとしんどいですよね。

また、一般的なソフトウェア実装プラクティスの話でもあります。同じようなコードを複数のリポジトリで何度も書くのは不毛ですし、間違いも起きるかもしれません。 「読み出す環境変数の名前を間違えて正しくメトリクスが取れなかった」みたいなことが起きる可能性もあります。自分は3回くらいやりました。

...と、このように、プロダクト自体の開発以外にも「忘れたら困る」「何回も書きたくない」「間違えたら困る」みたいなコードは意外とたくさん存在します。 このような **「新しくマイクロサービスを作る際に考えないといけないこと」を減らし、本来実現したいドメインに集中できるようにする** ために、 **ウォンテッドリーのすべてのマイクロサービスが備えるべき機能を扱いやすい・統一的な形で提供する**のが "servicex" という共通ライブラリの責務です。

この Why と What を踏まえて、servicex リポジトリの description には "**Microservices governance through code.**" と書いてあります。 この文言は書籍 [Building Microservices](https://www.oreilly.com/library/view/building-microservices/9781491950340/)（日本語版: [マイクロサービスアーキテクチャ](https://www.oreilly.co.jp/books/9784873117607/)）の2.6章のタイトル "Governance Through Code"（コードを介したガバナンス）から取られています。

## コアとなる機能：入出力フックとコンテキスト伝搬

ここまでで「マイクロサービス共通ライブラリ "servicex" とは何のために・何を提供するのか」を紹介してきました。 このライブラリを実際に2018年ごろに作り始めてからいままでのあいだに、起きたことを踏まえて、「共通ライブラリがあることで何が起きるか」というのを掘り下げていきます。

さて、前の節で servicex が提供するものに "Observability 関連" を挙げました。 具体的には「Metrics, Logs, Traces を収集するための機能」です。これを達成するためには何が必要になるでしょうか。自分は以下の3項目が重要なのではないかと考えています。

* Observability を網羅的に高めるためには、**すべての入出力にフックできる**必要がある
* 入力と出力を正しく紐付けるためには、**入力のコンテキストを出力部まで伝搬している**必要がある
* マイクロサービスをまたいでログ・メトリクス・トレーシングを関連付けるには、**すべてのマイクロサービスが上記の性質を満たしている**必要がある

入出力 という表現をしましたが、これはマイクロサービスが「どんなリクエストを受けて」「どうやってリクエストを飛ばすか」ということです。 ウォンテッドリーではマイクロサービス間通信に JSON over HTTP だけでなく gRPC を推進しているほか、非同期なやりとりには Cloud Pub/Sub を利用しています。 また、当然 RDB などのデータストアを持つマイクロサービスも多数存在します。ほかにも Valkey や Elasticsearch など、いくつかのミドルウェアを利用しています。 これらのマイクロサービス間通信の入出力やミドルウェアへの出力にフックし、観察することでリクエスト全体が追跡可能になったり、障害発生時にどこで異常が起きているのかを発見しやすくなるのです。

![hexagonal-architecture-like-input-and-output](/files/-MhWAOKcD1uSH3YiAqOu)

この **「すべてのマイクロサービスが」「すべての入出力にフックし」「入力のコンテキストを出力まで伝搬できている」** という特性があることで、 ウォンテッドリーではシステムの Observability の向上以外にもいくつかの恩恵を受けることができました。

* 事例: Network Observability 向上のための PoC
  * （社員向け情報: wantedly/infrastructure#4622）
  * マイクロサービス間通信のネットワークエラーの発生割合を可視化するために、servicex にデータ収集機構を実装した
  * サービスメッシュの導入を念頭に置きつつ、とりあえず PoC として小さく始めるために共通ライブラリを利用した例
* 事例: HTTP Header / gRPC Metadata 伝搬
  * （社員向け情報: wantedly/dx#89）
  * Feature Flag や開発時の特殊な Request Routing のために、条件を満たす HTTP header / gRPC metadata を後続のマイクロサービスに伝搬させる必要があった
  * 関連: [Istioを使って「Fast、Dependency-Agnostic、Isolated」な開発体験を実現した話](https://www.wantedly.com/companies/wantedly/post_articles/241506)

理想的には Sidecar conatiner や Service mesh などアプリ外のレイヤで解決できる/すべき問題も含まれているかもしれませんが、 「**ぱっと導入して価値検証できる・高速なフィードバックサイクルを回せる**」という点において共通ライブラリに優位性がありました。 また、アプリケーション中でこれらのコンテキストが取り出せることで、adhoc なイベントログ収集などでも活用できます。

## 言語・フレームワークサポート

「すべてのマイクロサービスが」「すべての入出力にフックし」「入力のコンテキストを出力まで伝搬できている」を満たすために、 ウォンテッドリーのバックエンドで利用している Ruby・Go・Python・Node.js に servicex の実装が用意されています。 一方で、すべての機能をすべての言語でサポートしていると大変なので、現状メジャーなユースケースのみのサポートとしています。

| -                                                                                             | HTTP server/client | gRPC server/client | Pub/Sub |
| --------------------------------------------------------------------------------------------- | :----------------: | :----------------: | :-----: |
| Ruby / Ruby on Rails                                                                          |         ✓/✓        |         ✓/✓        |   ✓/✓   |
| Go / [grapi](https://github.com/izumin5210/grapi), [subee](https://github.com/wantedly/subee) |         ✓/✓        |         ✓/✓        |   ✓/✓   |
| Python                                                                                        |         ✓/×        |         ✓/×        |   ×/×   |
| Node.js                                                                                       |         ✓/✓        |         ×/✓        |   ×/×   |

* フル機能（あらゆる通信方式で入力にも出力にもなれる）のは Go と Ruby のみ
* Python は誰にも依存しない「関数」的なマイクロサービスが多いので、 Server として必要な機能のみ存在
* Node.js も基本的には Python と同じだが、SSR server や GraphQL BFF があるため Client になる必要もある

ウォンテッドリーでは Ruby と Go が最もよく利用されるため、この2つに関してはフル機能の servicex がサポートされています。 対して Python や Node.js は限定されたユースケースでのみの利用となっているため、それをカバーする範囲のみのサポートとなっています。

上記の表で `×` になってたりカバーされてない領域に手を出す場合は、servicex も一緒に作りメンテしていく覚悟が必要になります。 ハブになるマイクロサービスでコンテキスト伝搬が切れてしまうと、それより後ろのすべてのマイクロサービスすべてに影響がでるため、特定のチームだけの問題ではありません。

### メンテナンス体制

servicex 全体のデザインに関してオーナーシップを持つ人間が3人と、各言語実装ごとにそれぞれ2〜4人の CODEOWNERS が設定されています。 言語実装のオーナーは各プロダクトドメインから1人ずつ選出されており、 コードレビューは[GitHub が Round Robin で割り当ててくれるやつ](https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/managing-code-review-assignment-for-your-team)により持ち回りで担当しています。

## まとめ

* servicex とは
  * Why: 新しくマイクロサービスを作る際に考えることを減らし，本来実現したいドメインに集中できるように
  * What: ウォンテッドリーのすべてのマイクロサービスが備えるべき機能を扱いやすい・統一的な形で提供する
  * 具体的に : Observability まわりのセットアップや、あらゆるマイクロサービスで共通で使う機能が提供される
* servicex のコア機能
  * すべてのマイクロサービスが」「すべての入出力にフックし」「入力のコンテキストを出力まで伝搬できている」状態を達成する
  * これによってインフラレイヤに機能を入れる前の PoC を小さく始めたり、コンテキスト伝搬を活用した便利機能が実現できる

それぞれの言語実装で簡単に使えるようにする工夫などもあって実装の話も興味深いのですが、本章では割愛しました。ぜひ実装されているコードを手元で読んでみてください。

#### 話を聞きに行きたい

* Slack: [#microservices](https://wantedly.slack.com/archives/CARTLEXR9)
* GitHub: `@wantedly/servicex-owners`

#### もっと知りたい

* [マイクロサービス共通ライブラリで “Governance through code” を実現する | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/311135)


# CronJob

ウォンテッドリーでは多くのマイクロサービスがバッチ処理を [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) として Kubernetes 上で動かしています。 CronJob は指定した時刻に Job を起動するだけのシンプルなリソースに見えますが、実際の運用では「同じ Job が複数回動く」「予定時刻に動かない」といった挙動に出くわします。 この章では、こうした挙動を踏まえて CronJob を書くときに気をつけることと、社内に用意されている検知・通知の仕組みについて説明します。

CronJob / Job という Object そのものの位置づけについては [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction) を参照してください。

## CronJob を書くときに気をつけること

### 冪等に書く

Job は同じスケジュールに対して複数回実行される可能性があります。 後述の二重実行に加え、`backoffLimit` に基づくリトライによっても同じ Job が複数回走ります。 同じ入力に対して何度実行しても結果が変わらないように書いてください。

### 長時間実行する処理は避ける

CronJob が起動した Pod はインフラ側の都合 (ノードの入れ替えなど) で中断される可能性があります。 長くても 3〜4 時間程度におさまるよう処理を分割してください。

### `startingDeadlineSeconds` を設定する

CronJob のスケジュールどおりに Job を起動するという挙動は厳密ではありません。 [Kubernetes 公式ドキュメント](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) に書かれているとおり、コントローラの仕組み上、起動が予定時刻より遅延することがあります。 ウォンテッドリーでの実測でも最大で 70 秒程度遅れることが確認されています。

この遅延に対する許容範囲を制御するのが `startingDeadlineSeconds` で、「未実行のスケジュールを発見したときに何秒までは実行を試みるか」を指定します。 ウォンテッドリーでは原則として `startingDeadlineSeconds: 300` を設定することを推奨しています。 これより小さいと実行漏れが、極端に大きいと意図しない二重実行が起こりえます。 [kube generate](https://docs.wantedly.dev/fields/infrastructure/deploy-strategy-implement#kubernetes-manifest-no) で生成したマニフェストには自動で設定されるため、特別な理由がない限り上書きする必要はありません。

> \[!TIP] **スケジュール変更時の罠**
>
> `startingDeadlineSeconds` を設定していても、スケジュールを「より遅い時刻」に変更した直後は二重実行が発生することがあります。
>
> 例えば毎日 2:00 am に動いていた CronJob を毎日 3:00 am に変更し、その変更を 3:01 am に apply した場合を考えます。 このとき、コントローラは「3:00 am の今日のスケジュールがまだ実行されていない」と判断し、`startingDeadlineSeconds` の範囲内であれば追加で 1 回起動を試みます。 結果として、本来 1 日 1 回しか動かないはずの Job がその日だけ 2 回動いてしまいます。
>
> スケジュールをより遅い時刻に変更する際は、変更後の最初の実行時刻を過ぎてから apply するなど、適用タイミングに注意してください。

## 失敗とスケジュール漏れの検知

ウォンテッドリーでは CronJob 周りの異常を検知する仕組みを社内で運用しています。 検知できるのは下の 2 種類です。

* Job の実行が `backoffLimit` で指定された回数を超えて失敗したとき
* CronJob がスケジュールされた時刻に実行されなかった可能性があるとき

Job が失敗したとき、該当マイクロサービスのリポジトリには GitHub Issue が自動作成されます。 さらに `cronjob-failure.wantedly.com/slack_notification` Annotation を CronJob に書いておくと、上の 2 種類の異常がいずれも Slack に通知されます。 スケジュール漏れの検知には [Honeybadger Check-Ins](https://www.honeybadger.io/check-ins/) を利用しており、CronJob のスケジュールを変更すれば Check-Ins のスケジュールも自動で追従します。

Annotation の詳しい仕様や通知メッセージの構造は [CronJob 失敗通知基盤 (internal)](https://dev-docs.wantedly.com/k8s/cronjob-failure-notification) を参照してください。

## 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)

## もっと知りたい

* [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction) — CronJob / Job という Object そのものの位置づけ
* [Kube](/fields/dev-tools/kube) — `kube` コマンドで Job を確認する方法
* [CronJob 失敗通知基盤 (internal)](https://dev-docs.wantedly.com/k8s/cronjob-failure-notification) — Annotation の詳細仕様、関連リポジトリ、社内サービス用 DSL
* [スケジュールジョブ (CronJob) の実行失敗通知基盤における成功と失敗 - Speaker Deck](https://speakerdeck.com/unblee/success-and-failure-in-scheduled-job-execution-failure-notification-infrastructure) — 通知基盤の設計の経緯


# 非同期メッセージング処理入門(未執筆)


# バッチ処理入門(未執筆)


# Infrastructure


# Infrastructure Squad

## Why

Infra Squad 『プロダクト開発の価値を高速に信頼性高く出力し続ける』をミッションに掲げています。

Dev Tribe では日夜プロダクト開発を行い、ユーザーがシゴトでココロオドルための価値を創出しています。 そのプロダクトの価値を絶え間なくユーザーに提供しつつ、プロダクト開発で生み出した新たな価値を高速に出力するための『システム基盤 / プラットフォーム』を、プロダクト開発と同様に開発し続ける必要があります。

## How

開発チームが Ownership を持って Dev, CI/CD, Ops (Monitoring + On-Call) を行えるように、我々は Platform として Tool/Library/System/Infra を用意します。開発チームが Platform を利用することで、自然と "強いシステム" が "高い生産性" で実現されるようにします。

* "強いシステム" の実現（= Site Reliability）
  * Performant
  * Reliable
  * Maintainable
  * Secure
  * Efficient
* "スケーラブルな開発組織を支える Platform" の実現（= Developer Productivity）
  * Tool/Library/System (= Build)
  * CI/CD (= Test, Deploy)
  * Observability (= Operation)

## What

"強いシステム" の実現のために、我々は "Platform 化" というアプローチを取ります。

具体的には以下のことに取り組みます。ただし、"強いシステム" の実現を考える際も基本的には「開発チームから使われるものを Platform として提供する」ことを念頭においています。

* "強いシステム" の実現
  * マイクロサービスアーキテクチャの強化
    * Availability, Resiliency, Scalability, Performance, Maintainability の向上
    * 以下の重要な関心ごとに対して「開発者が自然に意識できる、自然に適切なアプローチが取れる状態」の構築 => Platform 化
      * 適切な依存関係、組織にあったマイクロサービス境界、適切なインターフェースの宣言的管理、Fallback 機構の充実、通信の信頼性とパフォーマンス向上、非同期通信による結果整合性、バッチジョブの宣言的依存管理、バッチジョブの自動再実行、etc.
  * Cloud Infra の活用と Orchestration Layer の提供
    * AWS, Google Cloud
    * Kubernetes
  * セキュリティ対応・セキュリティレベルの向上
  * 効率的なマシンリソース利用
* "スケーラブルな開発組織を支える Platform" の実現
  * Tool/Library/System の開発・提供
  * 運用自動化 (k8s, terraform の利用)
  * CI/CD
  * Observability 強化 (Monitoring + Alert 強化, Error Rate 可視化)
  * 上記を Platform として提供してセルフサービス化、Ownership を持つ文化の構築
    * 障害検知 + 対応
    * Postmortem
    * Production Readiness Review

また、上記の実現を生産的に行うために、以下にも取り組みます。

* Toilの撲滅
  * 手動オペレーション効率化・自動化
    * Kubernetes Cluster Upgrade
    * K8s Manifest File 更新
    * Incident Issue
    * Postmortem
    * Production Readiness Review
    * 特定の Team のための Infra 構築
  * Support 運用の改善
* 優先度判断が漏れなく行われる体制構築
  * Project と直接紐づかない Task を貯める仕組みと消化する仕組み（= Backlog 消化 Day）

その他、Tool/System に出来てないものは開発チームに Support Issue を立ててもらって対応しています。

* Support

### KPI

* "強いシステム" の実現（= Site Reliability）
  * SLO 99.9 % 以上を維持する
  * 未来の変化を見据えた上で、強いシステムを作るための部品を提供
* "スケーラブルな開発組織を支える Platform" の実現（= Developer Productivity）
  * ヒアリングを通して課題を整理し、エンジニアリングによって解決

## 背景

以下のような Layered Architecture を考えた時、2014 年からは AWS を利用、2016年からは Kubernetes を利用と Layer を積み重ねてきました。Cloud + Kubernetes の Infra Layer を提供し、安定化させるということが行われてきて、年々良くなっています。

```
[Application (= Microservices)]
[Kubernetes]
[Cloud (= AWS, GCP)]
```

一方で、システムが巨大化し、機能が増え、開発チームが大きくなるほど、Application (= Microservices) のレイヤーの重要性が高まっています。この部分は開発チームが担う一方で、一方的に任せていれば良くなるというものではありません。特に、「開発 (Dev)」と「運用 (Ops)」が完全に分離してしまうと、そこに「新機能リリースのための変更（= Reliability 低下）」と「変更阻止（= Reliability 短期的に上昇）」という対立軸が生まれてしまい、開発組織が上手く行かなくなります。

そこで我々は、「Platform 化」というアプローチを取っています。開発チームが Ownership を持って Dev, CI/CD, Ops (Monitoring + On-Call) を行い、我々はそのための Tool/Library/System/Infra を用意します。また、考え方や Best Practice の啓蒙、組織構造の改善など、開発組織の文化・組織的な改善の取り組みも行います。

Platform を提供することで開発 Team の負担を軽減しつつ、開発 Team が Dev から Ops までを責任範囲とする事で強いシステムを作ることへの健全なインセンティブを用意します。具体的には、以下のような姿を開発 Team が実現出来るようにします。

* 強いシステム = 変化出来るシステム
  * <=> 弱いシステム = 変化出来ないシステム
    * 信頼性が低いために、システムを不安定にする「デプロイ」が出来ない => 信頼性が上がらない（むしろ下がる）という悪いループから抜け出せなくなる。
* どう強いシステムをどう実現するか?
  * \=> システムを複数のコンポーネントから作る（= マイクロサービス）
  * マイクロサービス単位で小さく高速に変化可能にする（= 変化出来るシステム）
  * マイクロサービスアーキテクチャ
    * 適切な依存関係
    * 組織にあったマイクロサービス境界
    * 適切なインターフェースの宣言的管理
    * Fallback 機構の充実
    * 通信の信頼性とパフォーマンス向上
    * 非同期通信による結果整合性

短期的には Platform は「開発 Teamの負担を増やす」ように見えますが、長期的にはシステムの安定性を高めつつ各チームが高速に開発を行えるようになるため、全体としての生産性は高まるはずだと考えています。Amazon, Google などの先人に倣う形です。

## Project

* "強いシステム" の実現（= Site Reliability）
  * Improve Monitoring and Alerting and Logging
  * Post-mortem / Incident Review
  * System Architecture
    * Event Driven Architecture
    * RPC framework (e.g. gRPC)
    * Service Mesh
  * Security
  * Efficiency
* "スケーラブルな開発組織を支える Platform" の実現（= Developer Productivity）
  * Dockerized
  * Implement tools for Developer
  * Faster CI/CD
  * Distributed Tracing
  * Mentenance Common library

## SLO/SLI

ウォンテッドリーでは、サービスの健全性を示す重要な指標として SLI を定義しています。また、目標として SLO も定義しています。

SLI は、「サービスを運営する上で重要な endpoint の成功したリクエストの比率」です。HTTP status code が 5xx 及び 405 (Method Not Allowed) 以外のリクエストを成功と見なします。SLO は「リクエストの 99.9% の成功」です。

## よく使うリポジトリ一覧

インフラメンバー以外の人に知っておいて欲しいリポジトリとその説明を示します。 リンクはすべて internal です。

* [wantedly/k8s（internal）](https://github.com/wantedly/k8s) Kubernetes cluster の設定
* [wantedly/kube-go（internal）](https://github.com/wantedly/kube-go) kube コマンド
* [wantedly/microservices（internal）](https://github.com/wantedly/microservices) microservice を作った際に更新するもの、作り方
* [wantedly/wantedly-terraform（internal）](https://github.com/wantedly/wantedly-terraform) AWS 等のインフラを構築
* [wantedly/secrets（internal）](https://github.com/wantedly/secrets) 秘匿する環境変数の設定
* [wantedly/docs（internal）](https://github.com/wantedly/docs) オフィス系の資料
* [wantedly/internal-proxy（internal）](https://github.com/wantedly/internal-proxy) nginx

## (参考) 利用している SaaS

* [AWS Console](https://wantedly.signin.aws.amazon.com/console)
* [Google Cloud Console](https://console.cloud.google.com)
* [Datadog](https://app.datadoghq.com)
* [PagerDuty](https://wantedly.pagerduty.com)
* [DNSimple](http://dnsimple.com/)


# プロダクト開発のための Kubernetes 入門

ウォンテッドリーのサービスを構成するマイクロサービスは、そのほとんどが [Kubernetes](https://kubernetes.io/ja/) クラスタ上で動いています。新しいコードのデプロイや、開発用クラスタで `rails c` のような一時的なコマンドを実行するといった日常の作業は、インフラチームの手を借りずにすべてのエンジニアが自分で行えるようになっています。

この章は、プロダクト開発を進めるうえで知っておきたい **Kubernetes の最低限の概念** と、**ウォンテッドリーでそれをどう使っているか** をまとめた入門です。インフラの専門知識は前提としません。普段はアプリケーションコードを書いているバックエンド・フロントエンドのエンジニアが、開発用クラスタを触れるようになることを目指しています。

このドキュメントで扱わないもの:

* `kube` コマンドの詳細な使い方
  * [kube](/fields/dev-tools/kube) を参照してください。
* Rolling Update や Canary などデプロイ戦略の詳細
  * [リリース・デプロイ戦略](/fields/dev-process/deploy-strategy-overview) を参照してください。
* ウォンテッドリーが利用しているクラウドサービスやネットワーク構成
  * [インフラ構成概要](/fields/infrastructure/infrastructure) を参照してください。

## なぜ Kubernetes を使うのか

ウォンテッドリーのプロダクトは多数のマイクロサービスで構成されており、それぞれが独立してデプロイされ、お互いに通信しています。このような構成では、以下のような課題が常に発生します。

* アプリケーションが落ちたときに作業が必要
  * 回復可能であれば自動で再起動してほしい
* アクセスに対して計算リソースが追いつかない
  * 自動で台数を増やしてほしい
* 新しいバージョンのアプリケーションをリリースした時にダウンタイムが発生する
  * ユーザーに影響なくバージョンを入れ替えたい
* 他のアプリケーションと通信する際にいちいち接続先を指定しなければならない
  * お互いの台数や IP の変化を意識せず通信したい

Kubernetes はこれらを **宣言的** に解決するための基盤です。例えば「このアプリを 3 台で動かしてほしい」と宣言しておけば、Kubernetes が常にその状態を保つよう動き続けます。台数が足りなくなれば自動で増やし、壊れれば自動で立て直す。こうした性質を **セルフヒーリング** と呼びます。

## コンテナと Kubernetes の関係

コンテナと Kubernetes は役割が異なります。コンテナはアプリケーションとライブラリを一つにまとめた実行環境であり、Kubernetes はそのコンテナを宣言した通りの状態に保ち続けるためのインフラです。

### コンテナとは何か

コンテナとは、アプリケーションのコードと、その実行に必要なもの一式（言語ランタイム、ライブラリ、設定ファイルなど）を一つのパッケージにまとめ、ホスト OS から隔離された状態で動かす仕組みです。ここでいう**ランタイム**とは、Ruby や Node.js のように、書かれたコードを実際に解釈・実行するためのソフトウェアのことです。Rails アプリを動かすには Ruby ランタイムが、Node.js アプリを動かすには Node.js ランタイムが必要になります。

こうしたランタイムやライブラリのバージョンは、ホストごとに微妙に違うことが珍しくありません。コンテナはこれらをすべてパッケージの中に同梱してしまうため、「自分の手元では動いたのに本番では動かない」という環境差分の問題が起きにくくなります。

このパッケージの設計図にあたるのが**イメージ**です。イメージは「どの OS をベースに、どのライブラリを入れ、どのコードを置き、どのコマンドで起動するか」を記述したテンプレートで、ファイルとして配布・共有できます。イメージを実体として起動したものが**コンテナ**です。クラスを定義してインスタンスを作るのと同じ関係で、1 つのイメージから何個でもコンテナを起動できます。

コンテナとイメージを扱うためのソフトウェアを**コンテナランタイム**と呼び、もっとも広く使われているのが Docker です。ウォンテッドリーでは、各マイクロサービスのリポジトリに Dockerfile（イメージの作り方を書いたファイル）が置かれており、CI がそれを元に Docker イメージをビルドします。

### コンテナはアプリケーションを「どこでも同じように動く単位」にする

イメージという形でアプリと依存をまとめてしまうと、開発者のローカル・QA・本番のいずれでも同じイメージをそのまま動かせます。ウォンテッドリーのマイクロサービスは Rails / Go / Node.js などランタイムが異なりますが、CI で Docker イメージとしてビルドされた後は、どのクラスタでも同一の成果物が動きます。

ただし、コンテナそのものは「アプリを 1 回起動する単位」を定義しているだけで、何台動かすか・落ちたらどうするか・他のサービスとどう通信するかには関与しません。手元で `docker run` してプロセスが落ちれば、それは止まったままになります。本番サービスとして安定して動かし続けるには、コンテナの外側で**誰かが面倒を見る仕組み**が必要になります。

### Kubernetes は「あるべき状態」を宣言として受け取り、現実を一致させ続ける

その仕組みが Kubernetes です。Kubernetes は、手続き（どう動かすか）ではなく状態（どうあってほしいか）を宣言として受け取るモデルを採用しています。例えば「このイメージから作ったコンテナを 3 台動かしてほしい」と宣言しておけば、Kubernetes は現在の状態を監視し続け、台数が 3 台になっていなければ自動で過不足を調整します。これが前章で触れた**宣言的**な解決であり、**セルフヒーリング**の正体です。

この分業によって、アプリケーション開発者が見るレイヤ（Rails / Go / Node.js のコード）と、Kubernetes が見るレイヤ（どのサーバーで何台動かすか、落ちたらどう立て直すか）はきれいに分離されます。エンジニアは成果物としてのコンテナイメージを差し出すことに集中でき、配置や再起動の責任は Kubernetes に委ねられます。

## Kubernetes の基礎知識

### なぜ Pod や Deployment が必要なのか

前章で見たように、コンテナは「アプリを 1 回起動する単位」を定義しているだけで、何台動かすか・落ちたらどうするか・他のサービスとどう通信するかには関与しません。本番でサービスを動かし続けるには、その外側に次のような役割を担う仕組みが要ります。

* コンテナを実際にどこかのサーバー上で起動し、必要なリソースを割り当てる役 → **Pod**
* 同じ種類の Pod を何台動かすかを管理し、壊れたら立て直す・新しいバージョンに入れ替える役 → **Deployment**
* 使い捨ての Pod 群に対して、安定した名前で通信できるようにする役 → **Service**
* クラスタの外（ユーザーのブラウザなど）から内部の Service へリクエストを通す役 → **Ingress**
* 多数のサービスのリソースを混ざらないように分けて管理する役 → **Namespace**

Kubernetes の基本概念は、いずれも前章の課題（セルフヒーリング・台数の自動調整 etc.）に対する具体的な答えになっています。

### 開発するうえで気をつけること

これらの概念の上でアプリケーションを書くときに、特に意識しておきたいことが 3 つあります。詳細は各節と後段で扱います。

* **Pod は使い捨てである前提でコードを書く**: Pod はいつでも壊れたり別のサーバーに移されたりします。ローカルディスクに長期保存したい状態を置いたり、「自分が同じ Pod で動き続ける」ことを期待した実装をしてはいけません。
* **新旧バージョンが一時的に同居することを意識する**: Deployment による入れ替え（Rolling Update）では、古い Pod と新しい Pod が短時間ですが同時に動きます。API・DB スキーマ・キャッシュ形式などは、両方のバージョンが同居しても壊れないように後方互換を保って変更します。
* **接続先は Service 名で指定する**: Pod の IP は再起動のたびに変わるため、別のマイクロサービスへ通信するときは Pod の IP ではなく Service 名を使います。台数を増減させても呼び出し側のコードを変える必要がなくなります。
* **マイクロサービスごとに 1 つの Namespace を割り当てられている**: `kube` コマンドは現在のディレクトリのリポジトリから対応する Namespace を自動的に判定します。

## Kubernetes のリソースの紹介

Kubernetes には非常に多くの概念がありますが、プロダクト開発を始めるうえで最低限おさえたいのは次の 5 つです。

### Pod — アプリケーションの最小単位

Pod は Kubernetes 上で動くアプリケーションの最小構成単位で、1 つ以上のコンテナをまとめたものです。多くの場合「1 つのアプリケーションサーバー = 1 つの Pod」と考えて差し支えありません。Pod は複数の **Node**（Pod を動かす計算リソース。物理マシンや VM のこと）上に分散して配置されます。

Pod は使い捨てを前提にした存在で、壊れたり再起動したりすると別の Pod に置き換わります。したがって、アプリケーションは「自分が同じ Pod で動き続ける」ことを期待してはいけません。

### Deployment — Pod の世代管理とセルフヒーリング

Deployment は Pod の管理役です。「この Pod を何台動かしてほしい」「今動いているバージョンを新しいものに入れ替えてほしい」といった指示を受け、実際の状態がそれに一致するように Pod を作ったり消したりし続けます。

たとえば Deployment に「3 台動かす」と宣言してあれば、Pod が 1 台壊れても Kubernetes が自動で新しい Pod を立ち上げて 3 台構成を保ちます。新しいバージョンをデプロイしたいときも、Deployment が古い Pod を少しずつ新しい Pod に置き換えます（これを **Rolling Update** と呼びます。詳細は [リリース・デプロイ戦略](/fields/dev-process/deploy-strategy-overview) を参照）。

関連する概念として、定期的に Pod を起動する **CronJob** や、一度だけ実行される **Job** もあります。バッチ処理や DB マイグレーションなどで使われます。

### Service — Pod への通信口

Pod は使い捨てで IP も変わり続けるため、別のアプリケーションが Pod に直接アクセスするのは現実的ではありません。Service はその間に立って、「この名前で通信すれば、裏にいる Pod のどれかに届く」というエンドポイントを提供します。

Service は Pod に対するロードバランサーとしても働きます。同じ Deployment が管理する複数の Pod に対して、Service 経由で通信することで自動的に分散されます。

### Ingress — 外部からの HTTP 入口

Service だけではクラスタの外からはアクセスできません。Ingress は HTTP レベル（L7）のルーティングを担当し、「このホスト／パスへのリクエストはこの Service に流す」というルールを定義します。ユーザーからのリクエストは、クラスタ外部のロードバランサー → Ingress → Service → Pod という経路で届きます。

### Namespace — リソースのグルーピング

Namespace は Kubernetes 上のリソースを分類するための名前空間です。同じ名前の Pod や Service も、異なる Namespace に置けば共存できます。操作権限の単位にもなっており、「この Namespace の中だけ触れる」といった制御もできます。

ウォンテッドリーでは **マイクロサービスごとに 1 つの Namespace を割り当てる** というルールで運用しています。これにより、リポジトリと Namespace が自然に対応づき、どのリソースがどのサービスのものか一目で分かります。

### Object の階層イメージ

ここまでに登場した Object の関係性を図にすると次のとおりです。

![](/files/-MhWAMvyxzFmtVkAR7bP)

## ウォンテッドリーにおける Kubernetes の使い方

ここからは、ウォンテッドリーで普段どのように Kubernetes を使って開発しているかを説明します。

### 3 つのクラスタ（Production / QA / Sandbox）

ウォンテッドリーでは用途の異なる 3 つのクラスタを運用しています。「QA環境」や「Sandbox クラスタ」、「本番環境」と呼ばれることもあります。

* **Production**: ユーザーからのアクセスを受ける本番環境です。
* **QA**: 本番デプロイ前の動作確認用クラスタです。原則として本番と同じバージョンのコードが動いており、QA テストチームや社内のビジネスサイドが動作確認に使います。
* **Sandbox**: 開発用クラスタです。

後述する `kube` コマンドは、第一引数で対象クラスタを明示します（例: `kube sandbox get pod`）。これは、うっかり本番に対して操作してしまう事故を防ぐためです。

### マイクロサービスと Namespace

前述のとおり、マイクロサービスのリポジトリごとに Namespace が 1 つ割り当てられます。`kube` コマンドは **現在のディレクトリのリポジトリから対応する Namespace を自動的に判定する** ため、基本的には「操作したいマイクロサービスのリポジトリに `cd` してから `kube` を叩く」という使い方になります。

### 環境変数は dotenv Secret で管理する

アプリケーションの設定値はコードに埋め込まず環境変数で渡すのが基本です。ウォンテッドリーでは各 Namespace に `dotenv` という名前の [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) を作り、そこに設定値を入れて Pod の環境変数に展開しています。

値の読み書きは `kube` 経由で行います。直接 `kubectl` で Secret を編集することは通常ありません。

### 自動デプロイの仕組み（CI/CD と Argo CD）

ウォンテッドリーのアプリケーションは、コードを master ブランチ（または main ブランチ）にマージすると自動的にデプロイされます。流れは大きく 2 系統あります。

* **アプリケーションコード**: CI が Docker イメージをビルドしてテストを実行し、成功すれば Kubernetes 上の Pod を新しいイメージに入れ替えます。
* **Kubernetes マニフェスト**: マニフェストのリポジトリに変更がマージされると、Argo CD が GitOps の仕組みでクラスタに反映します。

入れ替えは Rolling Update で行われるため、古いバージョンと新しいバージョンが一時的に同居する時間が存在します。Pull Request を設計するときは「バックエンドとフロントエンドの変更を分ける」「DB スキーマの変更とそれに依存するコードの変更を分ける」といった後方互換性の意識が重要です。

### Kubefork — 自分専用の開発環境

開発中のコードを試すたびに Sandbox に直接デプロイすると、他の開発者の作業を壊してしまうことがあります。これを避けるために、ウォンテッドリーには **Kubefork** という仕組みがあります。

Kubefork は、既存の開発用クラスタを擬似的にコピーして「自分専用の仮想クラスタ」を作る機能です。特定のリクエストだけを自分の fork に流すことで、他の人に影響を与えずに自分の変更を確認できます。Pull Request を作ると PR ごとに自動で Kubefork 環境が用意されるので、レビュワーに動作を見てもらうときにも便利です。

具体的な使い方は [Kubefork](/fields/dev-tools/fork) を参照してください。

## よく使う kube コマンド

ウォンテッドリーのクラスタを操作する入口は、社内 CLI の `kube` です。ここでは概念と紐付けた代表例だけを載せます。コマンドの詳細や拡張機能は [kube](/fields/dev-tools/kube) にまとまっています。

| やりたいこと                                    | コマンド例                               |
| ----------------------------------------- | ----------------------------------- |
| 動いている Pod の一覧を見る                          | `kube <env> get po`                 |
| Pod のログをリアルタイムで見る                         | `kube <env> tail`                   |
| デプロイ済みの Pod 上でコマンドを実行する（例: Rails console） | `kube <env> sh -d rails c`          |
| 自分のブランチのコードを Pod 上で実行する（例: DB マイグレーション）   | `kube <env> sh -c rails db:migrate` |
| 自分のブランチを Sandbox にデプロイする                  | `kube sandbox deploy -c`            |
| 環境変数を追加／更新する                              | `kube <env> dotenv set FOO=bar`     |
| 自分専用の仮想クラスタを作る                            | `kube <env> fork`                   |

`<env>` は `prod` / `qa` / `sandbox` のいずれかです。`kube` はカレントディレクトリから対象 Namespace を判断するので、**操作したいマイクロサービスのリポジトリに移動してから実行** してください。

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)

#### もっと知りたい

* [kube](/fields/dev-tools/kube) — 社内 CLI `kube` の概要と使い方
* [リリース・デプロイ戦略](/fields/dev-process/deploy-strategy-overview) — Rolling Update / Rollback / Canary / FeatureFlag
* [リリース・デプロイ戦略を支える技術](/fields/infrastructure/deploy-strategy-implement) — CI/CD と Argo CD の実装
* [Kubefork](/fields/dev-tools/fork) — 自分専用の仮想クラスタを作って開発する仕組み
* [インフラ構成概要](/fields/infrastructure/infrastructure) — Kubernetes 以外のクラウドサービスも含めた全体像
* [マイクロサービス共通ライブラリ "servicex" の紹介](/fields/the-system/servicex) — マイクロサービスの実装側の共通基盤
* [Kubernetes とは何か？（公式ドキュメント）](https://kubernetes.io/ja/docs/concepts/overview/what-is-kubernetes/)


# インフラ構成概要

ウォンテッドリーのシステムを構成する各マイクロサービスは全て Kubernetes クラスタ上で動いていますが、この Kubernetes クラスタはパブリッククラウドである Amazon Web Service (AWS) 上で動いています。またシステムに必要なコンポーネントとしては AWS だけではなく、同じくパブリッククラウドである Google Cloud Platform (GCP) も利用しています。

この章ではウォンテッドリーのマイクロサービスにおける代表的なインフラストラクチャの構成を説明し、どのようなミドルウェアやクラウドサービスを利用しているかを解説します。

## 構成図

構成図は次のとおりです。

![](/files/bj7l8nrqao46xAi46Wp4)

各コンポーネントの役割は次のとおりです。

| コンポーネント   | サービス名                     | プロバイダー       | 説明                                                                        |
| --------- | ------------------------- | ------------ | ------------------------------------------------------------------------- |
| データストア    | Aurora for PostgreSQL     | AWS          | 主要なデータベースとして使われる RDB。2024年以前のマイクロサービスについても、負荷が高いものは Aurora に移行済み。         |
| データストア    | RDS for PostgreSQL        | AWS          | 2024年以前のマイクロサービスで採用。                                                      |
| データストア    | ElastiCache for Valkey    | AWS          | 各マイクロサービスでキャッシュ用 KVS として採用。また、Rails では非同期ジョブ (Sidekiq) のためのメッセージキューとして採用。 |
| データストア    | Amazon OpenSearch Service | AWS          | 各マイクロサービスの検索エンジンとして採用。                                                    |
| データストア    | Elasticsearch on k8s      | -            | 各マイクロサービスの検索エンジンとして採用。 Amazon OpenSearch Service への移行を進めている。              |
| データストア    | Cloud Datastore           | Google Cloud | 一部のマイクロサービスのデータストアでのみで採用される KVS。                                          |
| データウェアハウス | BigQuery                  | Google Cloud | データ分析環境。ログ、DBのデータを格納。                                                     |
| コンピュート    | EC2                       | AWS          | Kubernetes を構成する Master / Node のコンピュート                                    |
| ネットワーク    | ALB                       | AWS          | Kubernetes Ingress を構成するロードバランサー                                          |
| ネットワーク    | CloudFront                | AWS          | コンテンツ配信                                                                   |
| ネットワーク    | Web Application Firewall  | AWS          | 攻撃リクエストからアプリケーションを保護するファイアウォール                                            |
| ネットワーク    | Route53                   | AWS          | DNS サービス                                                                  |
| ネットワーク    | DNSimple                  | DNSimple     | DNS サービス                                                                  |
| ストレージ     | S3                        | AWS          | コンテンツを格納するオブジェクトストレージ                                                     |
| メッセージキュー  | Cloud Pub/Sub             | Google Cloud | Event-Driven Architecture のためのメッセージキューとして採用。                              |

## コンポーネント解説

### データストア

#### RDB

ウォンテッドリーの各マイクロサービスにおける主要なデータストアは、AWS の リレーショナルデータベース (RDB) である Aurora for PostgreSQL または RDS for PostgreSQL が多く採用されています。 Aurora と RDS の使い分けは以下の通りです。

* 2024年以降のマイクロサービスでは原則として Aurora for PostgreSQL を採用しています。
* 2024年以前のマイクロサービスでは主に RDS for PostgreSQL を利用していましたが、負荷の高いものについては Aurora for PostgreSQL に移行済みです。

PostgreSQL 互換エディションを利用しているため基本的にはすべての RDB が PostgreSQL であると考えて構いません。

#### KVS

キャッシュのためのデータストアでは ElastiCache for Valkey が多く採用されています。 また、非同期ジョブの構成に Ruby on Rails の Sidekiq をよく使っていることもあり、そのメッセージキューとしても ElastiCache for Valkey が使われています。

一部のマイクロサービスでは、主要なデータストアに Google の KVS である Cloud Datastore を採用しているものもあります。

#### 検索エンジン

新規に導入される全文検索は基本的に Amazon OpenSearch Service で運用する予定です。

ただし、2026年以前に導入された検索エンジンの大部分はまだ Kubernetes 上の Elasticsearch で動いています。これらについても順次 Amazon OpenSearch Service に移行していく予定です。

さらに古い時代には Kubernetes ではなく EC2 上で Elasticsearch クラスタを組んでいたこともありました。

### データウェアハウス

データ分析基盤として BigQuery を採用しています。各マイクロサービスのアクセスログやイベントログがリアルタイムで連携されるほか、データベースやマスタが daily, hourly, または準リアルタイムで連携されています。 詳しくは [データ基盤入門](/fields/data/data-infra) を参照してください。

### メッセージキュー

Ruby on Rails の Sidekiq を使うために ElastiCache for Valkey を採用しています。 最近では Sidekiq の他に、マイクロサービス間の非同期メッセージングに Cloud Pub/Sub と Publisher/Subscriber アプリケーションの自前実装を採用することが多くなりました。

### ネットワーク

コンテンツ配信（CDN）は AWS の CloudFront を利用しています。そのバックエンドにはオブジェクトストレージである S3 か、もしくは画像処理系のマイクロサービスのロードバランサーを配置しています。

アプリケーションへのアクセスは AWS の Application Load Balancer (ALB) を 利用しています。この ALB は Kubernetes のリソースである Ingress からコントロールされています。

セキュリティ対策として AWS WAF(Web Application Firewall) が一部導入されています。脆弱性を突いた攻撃リクエストやボットによる大量アクセスを遮断し、アプリケーションを保護する役割です。

DNS には DNSimple と AWS の Route53 の2つを利用しています。

## インフラの構成管理

これらのクラウドサービスの構成や設定は、Terraform というツールを使ってすべてコードで管理しています。 これにより、インフラチームに限らず全ての開発者がコードを書いてインフラリソースの追加を行えるようになりました。インフラリソース追加・削除・変更の依頼は Issue (文章) ではなく、Pull Request (コード) で行います。

Terraform のコードは [wantedly/wantedly-terraform (internal)](https://github.com/wantedly/wantedly-terraform) で中央集権的に管理されています。

## インフラの変遷

現在の構成に至るまでの歴史については、[インフラ構成の変遷](/fields/infrastructure/infrastructure-history) を参照してください。

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)


# インフラ構成の変遷

ウォンテッドリーのインフラはサービス開始から2026年現在まで大きく5つのフェーズを経て進化してきました。

![ウォンテッドリーの開発プラットフォーム変遷](/files/fBv1sdy2FX2WxdEjf5cJ)

各フェーズの技術的な転換点とその背景を紹介します。

## フェーズ 1: PaaS 依存期 （2011〜2014年）

サービス開始時、Wantedly は Heroku 上で動作していました。 当時専任のインフラエンジニアは存在せず、インフラ機能を PaaS に委ねることで少人数チームでもプロダクト開発に集中できる体制を実現していました。

## フェーズ 2: AWS への移行 （2014〜2016年）

サービス利用者が増えレイテンシー等の問題が課題となり、2014年8月に AWS への移行を実施しました。 当時の Heroku には US と EU リージョンしかなく、特にモバイル環境でのユーザー体験のボトルネックになっていました。

![Wantedly on AWS ctonight New Challenges](/files/B6UJKHGGlEL8kfpjuCMR)

AWS に移行することにはなりましたが、Heroku のビジネスロジックの実装に集中できる開発体験は気に入っていたため、2013年にリリースされたばかりの Docker を採用し自律的にデプロイできるコンテナ基盤で環境を構築しました。

* ホスティング: Amazon EC2
* コンテナランタイム: Docker
* デプロイ基盤: Heroku ライクな内製 PaaS
* OS: Ubuntu 14.04 → CoreOS（2016年2月に移行。systemd によるサービス管理）
* デプロイツール: Capistrano と SSH を組み合わせた内製デプロイツール「sap」

初めは手動でインフラリソースの管理をしていましたが、2015年5月に Terraform を導入しインフラをコードで管理できるようになりました。 これによりインフラチームに限らず全エンジニアがインフラリソースの追加や変更を GitHub の Pull Request で行える体制が整いました。

## フェーズ 3: マイクロサービス化と Kubernetes 移行 （2015〜2018年）

2015年頃、新しいサービスの開発が頻繁に行われるようになりました。 リリースにはインフラチームがサーバー等の環境構築を行う必要があり、ボトルネックとなっていることが課題となりました。 今後もサービスが増えていくことを見据え、セルフサービスでリリースできる環境への移行を検討し始めます。

Kubernetes 採用前は内製 PaaS「paus」を開発しコンテナを管理していました。 しかし、サービス数の増加に伴い、安定したリソース管理やセルフサービスでのデプロイに限界が見え始めました。

2016年11月に Kubernetes を本番導入しました。 当時利用できるマネージド Kubernetes が存在しなかったため、EC2 上に自己管理のクラスタを構築していました。 その後 kOps によるクラスタ管理に移行しています。

マイクロサービスの増加に伴い本格的なオーケストレーション基盤が必要になり、全マイクロサービスの Kubernetes 化を段階的に進め、2018年8月に移行が完了しました。

* アーキテクチャ: モノリス Rails → マイクロサービス（Go, Ruby, Python, Node.js など各チームが技術選択）
* オーケストレーション: Kubernetes（EC2 上で自己管理）
* API 設計: gRPC + Protobuf による型安全なサービス間通信
* サービス間共通基盤: servicex（2018年1月誕生）
  * 構造化ロギング、分散トレーシング、コンテキスト伝播など共通機能を提供するライブラリ群
* 設計原則: 1マイクロサービス = 1リポジトリ = 1 Kubernetes Namespace

この時期に確立した原則が現在のアーキテクチャの骨格を形成しています。

### servicex について

マイクロサービス化が進む中で、ロギング・トレーシング・認証などの横断関心事を各チームが個別実装するコストが課題になりました。 `servicex` はこれらの共通基盤を一元化し、Go/Ruby/Python/Node.js など複数言語で提供しています。

詳しくは[マイクロサービス共通ライブラリ "servicex" の紹介](/fields/the-system/servicex)を参照してください。

## フェーズ 4: 運用成熟・Observability 強化期（2018〜2020年）

2018年8月に全マイクロサービスの Kubernetes 移行が完了した後、プラットフォームの安定化と運用品質の向上に取り組んだ時期です。 Kubernetes クラスタと社内共通ライブラリの servicex を拡張することによりできることを増やし、改善を進めました。

### Observability の確立

マイクロサービス間の障害調査やパフォーマンス分析が難しいという課題から分散トレーシングの導入を進めました。 始めはどのサービスも成熟していなかったため、ロックインを避けるために複数の基盤を併用し、検証を進めました。

### gRPC の標準化

マイクロサービス間通信の効率化のため、gRPC をサービス間通信の標準とする方針を策定しました。 Protobuf による型安全なインターフェース定義と、言語横断のクライアント生成を整備しています。

### Kubernetes クラスタの成熟

kOps を用いたクラスタ管理の標準化を進め、バージョンアップやクラスタ再構築を安全に行える体制を構築しました。 2020年2月には Production クラスタの再構築を完了しています。

内製ツール kube-go による Kubernetes マニフェスト生成の標準化も進め、ベストプラクティスを全リポジトリに一貫して適用できるようになりました。

### デプロイの改善

Canary Release を worker process にも拡張し、リリース時の安全性を向上させました。 Canary ごとの error rate 変化を検知する仕組みも構築し、デプロイ起因の障害の早期発見につなげています。

### SLI/SLO 運用の強化

SLO は2018年頃から運用していましたが、大規模障害に気付けず被害が拡大したことが課題になりました。 システム障害への体制強化のため、SLI/SLO に対する Design Proposal を策定して運用を再整備しました。

### 主な出来事

| 時期       | 転換点                                  |
| -------- | ------------------------------------ |
| 2018年3月  | Istio (service mesh) の検証開始           |
| 2018年9月  | gRPC の最適構成の検証開始                      |
| 2019年1月  | kOps による新 Production クラスタ移行計画        |
| 2019年4月  | RailsConf 2019 で分散トレーシングの発表          |
| 2019年7月  | Canary Release を worker process にも拡張 |
| 2019年9月  | gRPC をマイクロサービス間通信のスタンダードに策定          |
| 2019年11月 | 大規模障害対応体制の強化                         |
| 2020年2月  | SLI/SLO Design Proposal の策定          |
| 2020年2月  | Production k8s クラスタの再構築完了            |

## フェーズ 5: Platform Engineering 成熟期（2020年〜現在）

運用基盤が成熟したことを受け、Developer Experience と運用安定性のさらなる向上に注力するフェーズに入りました。

### 主な出来事

| 時期       | 転換点                                         |
| -------- | ------------------------------------------- |
| 2020年4月  | **kubefork** 開発開始（PR ごとのプレビュー環境基盤）          |
| 2020年5月  | **Argo CD** の検証開始（GitOps によるデプロイ管理）         |
| 2021年5月  | **PR ごとプレビュー環境**の本番導入                       |
| 2021年7月  | **Argo CD** の本格運用開始                         |
| 2021年9月  | **kOps → EKS 移行**プロジェクト開始                   |
| 2022年6月  | **EKS** (Elastic Kubernetes Service) への移行完了 |
| 2024年12月 | 監視基盤を **Datadog に統一**（New Relic を廃止）        |

### 現在のアーキテクチャ（2025年時点）

* ハイブリッドクラウド: AWS（主要ワークロード）+ Google Cloud（データ基盤）
* EKS（Elastic Kubernetes Service）中心のマイクロサービス構成
* Terraform による統一的なリソース管理（wantedly/wantedly-terraform）
* Datadog による統一監視

### kubefork について

PR ごとに独立したプレビュー環境を自動生成するための内製基盤です。 レビュアーが実際の動作を確認できるため、レビュー品質とデプロイ安全性が向上しました。 詳しくは [Kubefork](/fields/dev-tools/fork) を参照してください。

| フェーズ                    | 時期         | キーテクノロジー                                   |
| ----------------------- | ---------- | ------------------------------------------ |
| PaaS 依存                 | 2011〜2014年 | Heroku                                     |
| コンテナ化                   | 2014〜2016年 | AWS, Docker, paus, Terraform               |
| マイクロサービス・K8s 化          | 2015〜2018年 | Kubernetes（EC2）, servicex                  |
| 運用成熟・Observability 強化   | 2018〜2020年 | kOps, gRPC, OpenCensus, Canary Release     |
| Platform Engineering 成熟 | 2020年〜現在   | EKS, Argo CD, kubefork, Terraform, Datadog |

## 参考資料

* [ウォンテッドリーにおける Platform Engineering (speakerdeck, bgpat, 2025)](https://speakerdeck.com/bgpat/uontetudoriniokeru-platform-engineering)
* [Docker をフル活用したインフラの紹介と成長し続けるためのインフラ戦略 (speakerdeck, dtan4, 2016)](https://speakerdeck.com/dtan4/number-abejameetup)
* [Wantedly における プロダクト、技術、組織 7年間の進化 (speakerdeck, kawasy, 2018)](https://speakerdeck.com/kawasy/wantedly-niokeru-purodakuto-ji-shu-zu-zhi-7nian-jian-falsejin-hua)
* [社内の Platform を作る取り組み (speakerdeck, koudaiii, 2016)](https://speakerdeck.com/koudaiii/number-ocif16)
* [実践！マイクロサービス (speakerdeck, awakia, 2016)](https://speakerdeck.com/awakia/shi-jian-maikurosabisu)
* [インフラ構成概要](/fields/infrastructure/infrastructure)
* [Infrastructure Squad](/fields/infrastructure/infrastructure-squad)


# リリース・デプロイ戦略を支える技術

ここでは Kubernetes 上で起動する server や worker の deploy 戦略について説明します。 ウォンテッドリーにおける Git 及び GitHub の運用方法が前提となるので事前に [Git の慣習](/fields/dev-process/git-convention) を読んでおくと良いでしょう。

## デプロイ基盤

### GitHub Flow の実現方法

各 repository で CI/CD で下の２つのことを行うことで GitHub Flow を実現しています。

* 全 branch: Docker image の build (`kube ci-build`)
* main (master): Docker image の deploy (`kube <env> deploy`)

ここで `ci-build` は commit hash を tag にもつ docker image を作成して registry に保管し、 deploy コマンドが Kubernetes の Deployment に対する変更を反映することで rolling で deploy されます。

いわゆる CIOps という Push 型のデプロイです。

Docker image 以外の更新、つまり Kubernetes Manifest のデプロイについては Argo CD による GitOps という Pull 型のデプロイを採用しています。

![GitHub Flow を CIOps (Push 型) と GitOps (Pull 型) の二重構造で実現する全体像](/files/FlLS5HOhbQLYRxD5FcG2)

### Rolling Deploy

Rolling deploy では新旧２つのアプリケーションバージョンを一部ずつ置き換えていきます。 このため、アプリケーションにもよりますが最大で数分程度複数のバージョンが共存する時間ができることになるため、 すべての deploy は後方互換性をもたせる必要があります。 一例としてフロントエンドとサーバーサイドのコードを 1 つのコードベースで管理している microservice (wantedly/wantedly, wantedly/perk など) では API 追加とその API に依存する frontend は先に API のみを merge する必要があります。また逆に機能を落とす場合は先にフロントエンドからの依存がない状態を merge 仕切ってから API を落とす pull request を merge する必要があります。

![Rolling Deploy における Pod の段階的置換と共存期間](/files/sr2FFHnipsErQem5Sz6A)

### Kubernetes Manifest 管理

![kube-generate.yaml から Argo CD を経由して Cluster に反映されるまでの全体像](/files/U6p56Iu2bCGrEJkcRGD6)

#### Argo CD による GitOps

Kubernetes Manifest は [Argo CD](https://argo-cd.readthedocs.io/en/stable/) を導入して GitOps という Pull 型のデプロイを行っています。

もともとウォンテッドリーでは Docker image の deploy を CICD で行っていましたが、Deployment 以外の Kubernetes manifest は長らく手動での apply になっていました。 そのため、以下のような課題・問題がありました。

* リリースフローの自動化・高速化・簡易化を行い、インフラチーム以外でも Kubernetes Manifest の管理を可能にすることで開発・リリースの速度を早めたい
* apply 忘れなどで repository と実際に apply されている manifest が異なり、Kubernetes Cluster のメンテナンスや Upgrade のブロックになっていた
* apply 忘れなどで repository と実際に apply されている manifest が異なり、リリース時にアプリケーションエラーを引き起こすインシデントになった

そこで [Argo CD による GitOps を導入 (internal)](https://github.com/wantedly/infrastructure/issues/9506) し、特に kubernetes manifest の継続的なデプロイの自動化をゴールとして、これらの課題を解決しています。

ただし先述したとおり、アプリケーションコンテナのデプロイ は CICD で Kubernetes Deployment に対して変更を加えて Rolling Deploy を行っているため、docker image tag は GitOps の対象外となっています。 これは

* 課題に対して docker image tag の GitOps は一旦スコープアウトできる
* 障害対応時には `kube <env> deploy <commit hash>` コマンドで緊急切り戻しを行っているため、このフローのアジリティを壊したくなかった

といった背景がありました。

今後、docker image tag も含めて CIOps から　GitOps への移行を検討するなど、さらなるデプロイ戦略の改善を進めています。

#### Kubernetes Manifest の自動生成・更新

GitOps に加えて、ウォンテッドリーの microservice のための Kubernetes Manifest を自動生成して継続的に更新する仕組みがあります。 これにより、その repository に必要な Deployment, HPA, Service, PDB といった Kubernetes Resource を、ウォンテッドリーの microservice における推奨設定込みで用意できます。

generator は autoscale (Deployment + HPA + Service + PDB) のほかに ingress, cronjob, worker など複数用意されており、`kube generate` のサブコマンドから一覧を確認できます。

```
$ kube generate
Generate manifest files

Usage:
  kube generate [flags]
  kube generate [command]

Aliases:
  generate, g

Available Commands:
  autoscale          Generate manifest files for Deployment with Autoscale
  cronjob            Generate manifest files for Cronjob
  grpc               Generate manifest files for gRPC Deployment
  ingress            generate manifest files for Ingress to web pod
  internal-ingress   generate manifest files for Ingress resource for internal only
  namespace          Generate manifest files for Namespace
  rbac-kube-sh-by-ci Generate manifest files for resources to enable `kube sh` from CI
  redis              Generate manifest files for redis statefulset
  sidekiq-exporter   Generate manifest files for sidekiq-exporter
  worker             Generate manifest files for worker deployment

...
```

**`kube-generate.yaml` による宣言的な管理**

以前は `kube generate <subcommand>` を都度手元で実行したり、生成コマンドを列挙したシェルスクリプトを repository に置くことで manifest を生成していました。 この方式では「現在どの generator にどの設定で manifest が作られているのか」がコマンド履歴やスクリプトを読まないとわからず、再生成のたびに手元での操作も必要でした。

そこで現在は、生成したい manifest の内容を repository ルートに置く `kube-generate.yaml` に宣言的に記述する方式を推奨しています。 たとえば次のような内容になります。

```yaml
generates:
- namespace: {}
- autoscale:
    port: 8080
    healthcheckPath: /
- cronjob:
    name: example-cleanup
    schedule: "0 * * * *"
    image: example.com/cleanup:latest
    command:
    - bash
    - -c
    - "echo cleanup"
```

`generates:` 配下の各項目が利用する generator とその設定に対応しています。 この `kube-generate.yaml` をもとに `kube generate --update` を実行することで Kubernetes manifest 一式が再生成されます。 新たに generator を追加したい場合は `kube generate --save <generator>` を実行すると `kube-generate.yaml` に対応する項目が自動で追記されます。

`kube generate` がサポートしていない設定を加えたいときは次のいずれかの方法で対応します。

* `kube generate` の generator 実装そのものを拡張する
* `kube-generate.yaml` 上で patches を指定し生成結果に変更を加える
* 手動編集した manifest を `kubernetes/` ディレクトリ以下に追加する。 `metadata.annotations` に `wantedly.com/generated-by: manual` を付与しておくと自動再生成の対象から除外されます

**継続的な再生成**

`kube generate` のテンプレートは kube のバージョンアップに伴って更新されていくため、過去に生成した manifest が古いままにならないよう継続的に再生成する仕組みを用意しています。 Renovate のように最新の kube バージョンを検知して `kube generate --update` を実行し、差分があれば各 repository に manifest 更新の Pull Request を自動で作成します。 詳しく知りたい方はこちらも見てみてください。[継続的な manifest 更新の取り組み (internal)](https://github.com/wantedly/infrastructure/issues/7902)

## FeatureFlag

基本的にリリースブランチを持たない我々の開発手法ではコンフリクトを避けるためにコンスタントに main branch (master branch) へ merge していくことが重要です。 しかしながら main branch (master branch) で merge されると自動で deploy がなされるため、場合によっては変更をユーザーに見せたくないこともあります。 このような場合に FeatureFlag が有用です。 FeatureFlag は `boolean`, `string` のいずれかを返す method の返り値を request ごとに任意の値に変更できるウォンテッドリーの社内基盤です。 値の変更を許可しておきたい method を事前に FeatureFlag の library で wrap しておくと [Chrome Extension (internal)](https://github.com/wantedly/chrome-dev-extension) から override ができるようになります。 これを用いることでユーザーには機能を隠しておきつつ、qa 環境では簡単にその機能を顕在化させることができます。

![FeatureFlag の override は QA/Sandbox では Chrome Extension / Kubefork から行えるが本番では無効化される](/files/yaPsNOSMUMKBMSbXy5t9)

### Override 宣言のやり方

まず override できる method を指定する方法を示します。 現在 Rails と React がサポートされていますがそれ以外のフレームワークや言語に対応要望がある場合は Infra Squad に相談してください。

#### Rails

最新 version の servicex が install されていることを確認してください。

```ruby
module SomeNewFeature
  # この module を include すると module 内の任意の method が override 可能になる
  include Servicex::FeatureFlag::Trapper
  # 正規表現で特定の method に絞ることも可能
  feature_flag_trap_only /\?$/

  # 上記の正規表現に match するため override 可能になる
  # フラグ名は `SomeNewFeature.released?` となる
  def released?
    false
  end
end
```

```ruby
module SomeNewFeature
  # より低いレイヤの API を用いることで method でなくても任意の場所での override が可能
  def released?
    return true if some_condition

    # some-flag-name という名前で override 可能になる
    Servicex::FeatureFlag::Interceptor.intercept("some-flag-name") do
      false
    end
  end
end
```

※Rails の FeatureFlag 実装は本番環境では override されないことに注意が必要です。 [実装参照（internal）](https://github.com/wantedly/servicex-ruby/blob/2709bd9c4e794cebf6fb694bec75f215972d72d6/lib/servicex/feature_flag.rb#L13)

ただし本番環境でも FeatureFlag を使いたいケースは存在するため、実現に向けて検討中の段階となっています。 [Issue 参照（internal）](https://github.com/wantedly/dx/issues/382)

使用したいモチベーションが冒頭の「コンスタントに main branch (master branch) へ merge していくこと」だけならば 実装としては本番環境かどうかは予め環境変数で弾いておき、その上で Feature Flag の制御をするのがおすすめです。 こうすることで QA, Sandbox ではその制御を dev tool から flag を override して確かめることが可能です。

#### React

```typescript
// 別途 context provider を挿入する必要があるので詳しいことは各 repository を参照
import { useBoolFlag } from "@wantedly_private/frontendx";

export const SomeComponent: React.FC<Props> = (props) => {
  const featureEnabled = useBoolFlag("some-awesome-new-feature");
  if (featureEnabled) {
    // ...
  }
};
```

### Override のやり方

#### Chrome Extension

上記の方法で override 可能にした method は [Chrome Extension (internal)](https://github.com/wantedly/chrome-dev-extension) から override できます。

![Chrome Extension の popup を開いた表示](/files/-MhWAMx_RkDcGvRCkrDK)

qa 環境にアクセスして Chrome Extension の popup を開くと response の render に使われた flag の一覧が表示されます。 ここで変更したい flag の override を値を更新してページをリロードすることでサーバーの挙動を override することができるためリリース前の機能などを試すことができます。

#### Kubefork

より高頻度に確認したい場合は、特定の Flag を有効にしたサーバーを別の URL で serve することができます。 具体例として下のような状況が実現できます。

* 新しいデザインの LP を出すかどうかを `new-lp` という Flag で提供
* `https://new-lp.qa.wantedly.com` のような URL を作成
* ここにアクセスすると任意のブラウザにおいて Chrome Extension なしで `new-lp` が有効になっているものとして動作する

エンジニアチーム外の人にテストをお願いしたいときには特に便利です。

詳しい利用方法については [fork のドキュメント（internal）](https://dev-docs.wantedly.com/fork) の「特定の FeatureFlag が有効にしたものをテストしたい」のセクションを参照してください。

## Pull Request Preview

上記のように FeatureFlag を使うことでリリースブランチを作らないようにすることが基本方針です。 しかしながら依存 Library の upgrade やアプリケーションの構成の変更などどうしてもリリースブランチを作る必要があることもあるでしょう。 こういったケースでは Pull Request Preview が便利です。 任意の Pull Request において `/preview <env>` とコメントすると次の図のように URL がコメントされるためここにアクセスするだけで preview が実現できます。

![Pull Request Preview の例](/files/7kTw1rzooSOXfa9YNgOH)

ブランチに更新があると追従されるため、数週間以上に渡ってリリースブランチを維持しなくてはいけないときに特に有効です。 この機能のバックエンドは [マイクロサービスでもポチポチ確認するための Kubefork](https://www.wantedly.com/companies/wantedly/post_articles/313884) であるため、 ユーザーフェイシングでないマイクロサービスでも利用可能です。 主要な repository はサポートされていますが自分の管理する repository が対応しているかについては Infra Squad に問い合わせてください。

## Migration

GitHub Flow では main branch (master branch) に存在する commit はすべて deploy 可能であるべきです。 もちろんこれを完全に保証することは不可能ですが、自動デプロイを行っているため少なくとも最新の commit が deploy されて困る状態は許容できません。 したがって、データベースの migration に依存する commit の merge は migration のあとに行われる必要があります。

そこでこれを保証する CI として [pending-migration-checker（internal）](https://github.com/wantedly/pending-migration-checker) があります。

![pending-migration-checker による merge ブロックと migration 実行後の再 check フロー](/files/nltgHywzeIHG8zilqS8X)

![Pending Migration Checker の例](/files/-MhWAMxbipCI544UJN_J)

この check が落ちている場合は migration が必要です。 migration を `kube prod sh` などで実行して、`/check migration` とコメントすることで再度 check が実行されます。 詳しい設定方法などは上記の repository のリンク先を参照してください。

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)

#### もっと知りたい

* [マイクロサービスでもポチポチ確認するための Kubefork | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/313884)
* [wantedly/kube-go（internal）](https://github.com/wantedly/kube-go)


# トラブルシューティング: 何を見たいときに何を使うか

ウォンテッドリーのプロダクト開発チームは、ソフトウェア開発のライフサイクル全てにオーナーシップを持っています。これにより、ライフサイクルを通した学びのフィードバックがチーム内に蓄積し、全体が効率化することで、ソフトウェアの価値提供をより素早く行うことが可能になります。この考え方は [Netflix によって Full Cycle Developers](https://netflixtechblog.com/full-cycle-developers-at-netflix-a08c31f83249) と呼ばれて広く知られています。

開発するシステムの運用にも責任を持っているということは、インフラエンジニア以外であってもシステムに問題が発生した場合に調査・解決する能力が求められ、いわゆる [Embedded SRE](https://medium.com/slalom-build/the-many-shapes-of-site-reliability-engineering-468359866517) のような能力や立ち振る舞いが必要となります。

本章では「こういう事象のときはまずここを見る」を逆引きの形でまとめます。各ツールの詳細な使い方は [Honeybadger](/fields/infrastructure/troubleshooting/honeybadger) と [Datadog](/fields/infrastructure/troubleshooting/datadog) を参照してください。

| 事象                                   | まず見る場所                                                                                                                                                   |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| アプリケーションのエラーを調べたい                    | [Honeybadger](https://app.honeybadger.io/) のプロジェクト画面                                                                                                     |
| エラーが発生したリクエストの全体像を見たい                | [Honeybadger](/fields/infrastructure/troubleshooting/honeybadger#error-to-datadog-trace) 詳細画面の「Context」欄にある `datadogTraceUrl` から Datadog Trace へ         |
| 特定の API / エンドポイントの遅さを調べたい            | [Datadog APM](https://app.datadoghq.com/services) の Service Catalog                                                                                      |
| ALB のアクセスを Path / IP / UA などで集計したい   | [Datadog Logs](https://app.datadoghq.com/logs) (`service:elb` で絞り込み)、attribute が足りないときは BigQuery                                                         |
| DB のメトリクスを見たい                        | Datadog Dashboard [DB Metrics](https://app.datadoghq.com/dashboard/5wr-4m8-stu/db-metrics)                                                               |
| Redis のメトリクスを見たい                     | Datadog Dashboard [Redis](https://app.datadoghq.com/dashboard/etg-twr-4yd/elasticacheredis-for-wantedly)                                                 |
| Sidekiq のメトリクスを見たい                   | Datadog Dashboard [Sidekiq](https://app.datadoghq.com/dashboard/tiz-r8c-cck/sidekiq)                                                                     |
| HPA (Pod のオートスケール) の状態を見たい           | Datadog Dashboard [HPA](https://app.datadoghq.com/dashboard/33y-pcz-yaq/hpa)                                                                             |
| BigQuery の Slot 使用率を見たい              | Datadog Dashboard [BigQuery](https://app.datadoghq.com/dashboard/c2q-6fu-nfn/bigquery)                                                                   |
| (D)DoS 攻撃を疑うとき・WAF 経由のリクエスト数を見たい     | Datadog Dashboard [Request Count (WAF)](https://app.datadoghq.com/dashboard/j7k-bc5-acz/request-count-waf)                                               |
| Pod 内のプロセスごとのリソース消費を見たい (OOM 調査など)   | [Datadog Infrastructure: Processes](https://app.datadoghq.com/process)                                                                                   |
| 任意のメトリクスを時系列で確認したい                   | [Datadog Metrics Explore](https://app.datadoghq.com/metric/explorer)                                                                                     |
| Dashboard では表現しづらい複雑な分析を作成・共有したい     | [Datadog Notebooks](https://app.datadoghq.com/notebook/list)                                                                                             |
| fork した環境でエラーやトレースを追いたい              | [Datadog APM Traces](https://app.datadoghq.com/apm/traces) で `@fork-identifier` フィルター                                                                    |
| Pod が `CrashLoopBackOff` している原因を調べたい | Pod のログを [kube logs (internal)](https://dev-docs.wantedly.com/deploy/kube-usage) または [Loki (internal)](https://dev-docs.wantedly.com/loki/README-ja) で見る |


# Honeybadger

[Honeybadger](https://www.honeybadger.io/) は、主にアプリケーションで発生したエラーの記録・観測、エラーによるアラートの発火のために利用しているエラートラッキングサービスです。

Honeybadger にエラーを送信する仕組みは [servicex](/fields/the-system/servicex) で実現されています。

「観測したい事象に対してどのツールを使えばよいか」については [トラブルシューティング](/fields/infrastructure/troubleshooting) を参照してください。

## 使い方

トップページに行くと、Honeybadger でエラーをトラッキングしているプロジェクト (アプリケーション) が一覧になっているので、観測したいものを選択します。

![](/files/-MhWAN94UZJwvQvKycec)

プロジェクトを選択すると、以下のようなプロジェクト内で起こったエラー一覧が表示される画面に遷移します。ここでは同じ内容のエラーがグルーピングされて表示されるようになっており、今までに起こった回数や 1 時間以内に起こった回数などをざっくり把握できます。

**この画面でよく見るのは 1 時間以内に大量にエラーが出ているかどうか** です。もし大量に出ているエラーがあれば、それを選択して詳細画面へ進みます。

![](/files/-MhWAN954JRwxZM5GUn_)

各エラーの詳細画面は以下のようになっています。**例えばある特定の時刻からエラーが急増していることを発見し、その時刻付近に起こったイベント (e.g. デプロイ、DB の障害) を紐付けて仮説を立てていく** ことをよくやります。

![](/files/-MhWAN96AwzAfdUmUQcF)

具体的にコード上のどこでエラーが発生したのかはバックトレースで確認します。

![](/files/-MhWAN97_rWcjaXBmzc2)

## エラー通知から、どのようなリクエストが原因でエラーが発生したか確認する <a href="#error-to-datadog-trace" id="error-to-datadog-trace"></a>

他のマイクロサービスからのリクエストでエラーが発生した場合、その API がどのような使われ方をしているか分かりにくく、Honeybadger だけを見てもユーザー影響の判断やデバッグが難しいことがあります。

そのような場合は、「Context」欄の中の `datadogTraceUrl` から、そのエラーが発生したリクエストが含まれる Datadog Trace ページを開くと、そのエラーがリクエスト全体のどの部分で発生したのか、ユーザーがどのようなリクエストをしたときに発生したエラーなのかを確認できます。

この `datadogTraceUrl` は [servicex](/fields/the-system/servicex) によって追加されています ([実装 (internal)](https://github.com/wantedly/servicex-ruby/blob/master/lib/servicex/honeybadger.rb#L18))。

![](/files/TS5oUiIkNLvEZjjpJmVx)

![](/files/l4OhNkeUkBRjCbOzJmW1)

Datadog Trace の見方については [Datadog](/fields/infrastructure/troubleshooting/datadog) を参照してください。


# Datadog

[Datadog](https://www.datadoghq.com/ja/) は、主にインフラレイヤのミドルウェア (or 任意) のメトリクスを記録・観測するために使われています。また、メトリクスの変化を元に事前に決められたしきい値を超えたことを検出してアラートを発火させることも行っています。

「観測したい事象に対してどのツールを使えばよいか」については [トラブルシューティング](/fields/infrastructure/troubleshooting) を参照してください。

## 使い方の基本

どの画面でも選択したパラメータが URL に追加されていくようになっており、実質パーマリンクとして機能します。このため、障害対応時に何かしらのメトリクスを観測したとき **自分の考察・解釈とは別に観測した事実として Datadog のスクショと URL を Slack や GitHub の Issue に貼り付ける** ことを意識しています。

「メトリクス」は Datadog の基本的な観測単位で、それらを組み合わせて Dashboard や Notebook のような高度な観測が実現されています。以下は技術的な構成順に紹介しますが、エンジニアが日常的に見るのは Dashboard が中心です。

## Metrics Explore

Datadog が記録しているメトリクスを絞り込んで時系列で表示する一番基礎的な機能です。Dashboard になっていなかったり、新しく追加したメトリクスはここで確認します。

![](/files/-MhWAN98LnDnizPa9CK2)

## Notebooks

Dashboard では表現しづらい複雑な分析 (Timeshift や Forecast などの関数を組み合わせたグラフなど) を作成・保存し、他人と共有するために使います。

![](/files/-MhWAN99kFk7iCOe9BCb)

## Dashboards

グラフの配置が柔軟にでき、各グラフのパラメータの一括設定もできる、より高機能な Notebook のようなものです。すでに過去のインフラチームやエンジニアが、観測したいプロダクト・ミドルウェアごとに作成しています。おおよそ必要なものはすでに揃っているので、新しくプロダクトやミドルウェアを導入するなどしない限り、新規作成する機会は少ないです。

![](/files/-MhWAN9AjoCboTSCyUbW)

エンジニアは主に Dashboard 単位でメトリクスを観測します。よく見る Dashboard には以下があります。

### DB Metrics

AWS の RDS (PostgreSQL) の各種メトリクスが観測できます。

![](/files/-MhWAN9BBEZyJbRCVC4i)

### BigQuery

ウォンテッドリーでは BigQuery の定額プランを契約しているものの、同時に使えるリソース (Slot) は有限なので、それらのリソースを観測するために使います。BigQuery が重いと感じたら Slot を使い切っている可能性があるので確認してみるとよいでしょう。

![](/files/-MhWAN9C6bJgrRfZZJW4)

### Request Count (WAF)

WAF を通過した HTTP リクエスト数を可視化する Dashboard です。主に (D)DoS 攻撃を疑うときに、リクエスト数の急増や送信元 IP / Path / Host の偏りから攻撃の有無や規模を確認するために使います。

## Infrastructure: Processes

Kubernetes の Pod (Container) 内で実行されているプロセス名 (コマンド) ごとに、CPU やメモリといったリソースの消費量が可視化できます。最近 OOM killer で殺されることが多いと感じたら、これでどの時間帯にどのプロセスがリソースを大量に消費しているのか調査できます。

![](/files/-MhWAN9DibA0KYz9Ivjs)

## APM

APM の Service Catalog から観測したいアプリケーションを選択して、エンドポイントごとのパフォーマンスや、リクエストのどの部分でどのくらい時間がかかっているか、どのマイクロサービスへリクエストが行われているかなどを確認できます。

### 自分が fork したリクエストのみ確認する

APM の Traces から、すべてのアプリケーションに対する trace を確認できます。ここで画像のように `@fork-identifier` フィルターを指定すると、fork したリクエストのみに絞ることができます。

fork については [fork (internal)](https://dev-docs.wantedly.com/fork) を参照してください。

![](/files/QcS3dnSImspGy7Ckw4Yn)

## 話を聞きに行きたい

* Slack: [#infra (internal)](https://wantedly.slack.com/archives/C010V922570)


# Data


# データ基盤入門

このドキュメントの目的はウォンテッドリーにおけるデータ基盤の存在意義と構成要素を紹介し、データを活用するための基礎的な知識を理解してもらうことです。

## データ基盤の存在意義

経営においてデータを使って判断することは必要不可欠であり、その正確性とスピードがビジネスの勝敗を決めます。 ウォンテッドリーのデータ基盤の存在意義の1つは、そのビジネスにおける意思決定の正確性とスピードをサポートすることです。

* 例：紙や Excel でのデータ分析・管理からの効率化。分散されたデータでの分析の難しさの解消。ユーザー行動データを元にしたプロダクトの価値検証・開発。

また Wantedly のプロダクトでは、データそのものがユーザーにとっての価値を生み出します。 そのユーザーにとっての価値を絶え間なく提供し続けることも、ウォンテッドリーのデータ基盤の存在意義の1つです。

* 例：ユーザー行動を元にアイテムを提案し、ユーザーの意思決定支援をプロダクト価値として提供する。

## データ基盤概略

ウォンテッドリーデータ基盤の概略図を以下に示します。

![](/files/I7GgstB6nOVlmmjkVUiB)

## データウェアハウス (コンピュートとストレージ)

データを貯めて分析クエリを実行するデータウェアハウス製品として BigQuery を採用しています。

ウォンテッドリーの BigQuery 上のデータは『Raw Layer』と『Transform Layer』で層が分けられ、テーブルは『source』『staging』『mart』の3種類を定義しています。

データ層

* Raw Layer: 生のデータ群を扱う層
* Transform Layer: 変換したデータ群を扱う層

テーブル種別

* source: データソースの構造に準拠したスキーマとテーブル
* staging: データをモデリングする上での最小単位であり source と1対1の関係。一貫性のあるカラム名や構造にするための加工を施したもの
* mart: 処理手順やエンティティを表現するモデル

もともと実態としては『source』データと集計したデータくらいの分けしかありませんでしたが、こういった分類を新たに定義して2022年から運用をはじめました。

主に分析業務の効率化と目的として以下を狙っています。

* データ分類を定義して目的のデータを発見しやすくする
* モデリングや前処理の手法、分析者の知見を適切な場所にコミットできる環境をつくる

## データ収集コンポーネント

文字通り、データを収集してデータウェアハウスに蓄積するコンポーネントです。 この方法で集めたデータは『source』テーブルになります。 データの発生源によってそれぞれデータ収集コンポーネントが存在します。

### モバイルアプリ イベントログ

Android / iOS といったモバイルアプリのイベントログは、Google Firebase Analytics といった SaaS や、内製のデータ収集コンポーネントである frolog (後述) を使ってリアルタイムで行われます。 モバイルアプリにおいてユーザーのイベントログを新しく追加したい場合は、それぞれのツールの SDK に沿ってアプリケーションコードを書くことになります。

### フロントエンド イベントログ

フロントエンドのイベントログ収集では frolog と呼ばれるデータ収集コンポーネントを内製して利用してリアルタイムで行われます。 フロントエンドにおいて新しくイベントログを追加したい場合は、この frolog の client ライブラリに沿ってアプリケーションコードを書くことになります。 イベントログ収集において機能が足りない場合はこの frolog に機能追加することになります。

### アプリケーションサーバー イベントログ

アプリケーションサーバーのイベントログ収集は fluentd を経由してリアルタイムで行われます。 このアプリケーションからイベントログを fluentd に転送する実装は社内ライブラリである servicex にまとめられており、アプリケーションに servicex を導入することでアクセスログや任意のイベントログを Fluentd に転送できます。 アプリケーションサーバーのイベントログを新しく追加したい場合は、各言語の servicex を利用してアプリケーションコードを書くことになります。

### データベース / テーブル

データベースのテーブルをデータウェアハウスに収集するには、内製モジュールである analytics の RDB import 機能を利用します。 Ruby で書かれており、DSL を記述することで任意のデータベースのテーブルをデータウェアハウスに転送することができます。 実行スケジューラーの実装は Kubernetes の CronJob になっており、DSL を追加してデプロイすると CronJob に変換されます。だいたい daily か hourly で設定することが多いです。

データベース内の個人情報や機密情報など、社内でも分析用途での取り扱いが禁止されているデータについては、このモジュールで特定カラムの除外・マスキングを行っています。

また analytics にはデータベースの他に、Salesforce やその他 SaaS のデータを取り込む機能が実装されており、ビジネスチームもこの analytics の DSL を書いています。

データベースからデータウェアハウスへのエクスポートには、前述の analytics のほかに以下の2つのミドルウェアが動作しています。

* rds-exporter は Aurora and RDS for PostgreSQL の機能を使ってデータベースダンプを Parquet 形式で出力し、 BigQuery にインポートします。
* pg-exporter は PostgreSQL の論理レプリケーションストリームを受信し、変更を BigQuery CDC で連続的に適用します。そのため、準リアルタイムで同期を行うことができます。また、変更ログの形でデータウェアハウスに保存することもできます。

## データ変換ツール

『source』から『staging』や『mart』といったテーブルやビューに変換するツールです。ウォンテッドリーでは以下のツールを採用しています。

* dbt(data build tool): SQL でモデルや前処理を定義し、実行することでモデルの実態（テーブル）を生成するツール。モデル同士の依存関係を解決できる。
* analytics: SQL でモデルを定義し、実行することでモデルの実態（テーブル）を生成するツール。内製。Ruby の DSL でモデルと実行スケジュールを記述する。
* BigQuery View (bqv): Mart や Staging を簡易的にビューで実現する。クエリを書くことでビューを管理する [rerost/bqv](https://github.com/rerost/bqv) を開発。
  * bqv の詳細は 『 [BigQueryのクエリをコードとして管理する | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/286165) 』 へ。

もともと analytics はウォンテッドリーデータ基盤の初期に開発されたもので、データ収集・変換・出力がまとまったコンポーネントです。 長年データ変換処理を運用していくツラミや生産性向上の観点から dbt が新たに導入されました。

参考 issue: [dbt という Alternative bqv-catalog, analytics の導入検証を進めています dev#1321 (internal)](https://github.com/wantedly/dev/issues/1321#issuecomment-1108108811)

## BI ツール

BigQuery 上のデータを活用して可視化するツールです。Looker と Looker Studio がよく使われています。

継続的にモニタリングしたり、複数人で共有する場合は Looker でモデルを作り込むほうがおすすめです。 クエリ結果を雑に可視化したい場合は、Google Looker Studio がおすすめです。

詳しくは Looker 入門 で解説される予定です。

## ワークフローエンジン・ジョブスケジューラー

データ基盤における処理は「定期実行バッチ」という体系を取るものが多いです。そのためのジョブスケジューラーが必須になります。 analytics での RDB import や データ変換ジョブのスケジューラーとしては Kubernetes CronJob を利用しています。

ジョブスケジューラーに加えて、高度なデータ変換や集計、特に機械学習では、マイクロサービスの境界を超えたデータやジョブの依存関係を持つ必要があります。 そのため、ジョブの依存関係を定義して実行してくれる Argo Workflow というワークフローエンジンが導入されています。Argo Workflow には CronWorkflow というジョブスケジューラーも内包されており、これをメインで利用しています。 詳しくは『 [Data Scientist 向けに Wantedly の推薦基盤を支える Argo Workflow や Kubernetes などのインフラ、New Relic や Datadog などの SaaS を紹介する速習会をしました！ | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/301394) 』も見てみてください。

## メタデータ・スキーマ管理

BigQuery のメタデータ管理・検索ツールである DataCatalog を活用しています。データ分析における効率を上げるために、データ収集コンポーネントに設定を入れることでデータのメタデータを収集・DataCatalog で検索できるようにしています。

イベントログテーブルの宣言的な管理として bqs というものを作っています。bqs では Ruby の DSL でテーブルのメタ情報を書くとテーブルが作成されます。 この bqs にテーブル説明やカラム説明を記述することで、DataCatalog でテーブルの検索が可能になります。

データ変換ツールでもモデルの説明やカラム説明を設定することができ、同様に DataCatalog で検索できるようになります。

メタデータ（テーブル説明やカラム説明、コードの意味合い）は、データ分析を効率的に行う上で重要な役割を果たしています。 何者かわからないテーブルは活用のしようがなく、不要なコミュニケーションコストを発生させます。 新しくデータを作るときは、メタデータをしっかり設定するようにしましょう。

## システムからのデータ基盤の利用

レコメンドシステムやメール施策におけるターゲットユーザーの抽出など、システムからデータ基盤にアクセスすることがあります。 この場合はレコメンドシステムやメール配信ジョブが所属する各マイクロサービスから、BigQuery のデータを Pull する形を取ります。

BigQuery は通常のアプリケーションで利用されるリレーショナルデータベース (RDB) とは異なる特性を持っています。 たとえば、RDBではリクエスト単位で処理を行うため、高速なレスポンスが求められますが、BigQuery を代表とするデータウェアハウスでは、大量レコードの抽出・集計処理に特化しており、パフォーマンス特性が異なります。 したがって、データウェアハウスに対して RDB のような感覚でクエリを実行したりアプリケーションを実装するとパフォーマンスに問題がでたり予期せぬところで問題が発生します。 以下のポイントには気をつけましょう。

* BigQuery では1レコードを抽出するクエリを複数回実行するのではなく、1クエリでデータを取得してアプリケーション側で処理する
* BigQuery では 1レコードに対して**頻繁に**更新(UPDATE, DELETE)する処理はパフォーマンスや計算資源的に不利なため、なるべく行わない。大量レコードを一括で変更したり非同期で行うことを推奨する

## 歴史

* 2015 前半 DOMO 利用開始
* 2016 後半 分析基盤を TreasureData から BigQuery に移行
* 2016 後半 データロード・集計を行う wantedly/analytics を開発・導入
* 2016 後半 機械学習が People で運用開始
* 2016 後半 機械学習が Visit で運用開始
* 2017 後半 Visit の募集一覧のソート結果が BigQuery に保存され、過去分の再現がしやすくなる
* 2018 前半 アプリケーションのイベントログ転送を BigQuery への直接ロードから Fluentd 経由に移行開始
* 2018 前半 BigQuery スキーマ管理として bqs を開発・導入
* 2019 前半 ワークフローエンジンとして Argo Workflows 導入
* 2019 後半 全てのイベントログが Fluentd 経由で BigQuery に蓄積されるようになる
* 2019 後半 BigQuery View 管理のための bqv を開発・導入
* 2020 後半 Looker導入/DOMO解約
* 2022 前半 データ変換に dbt を導入
* 2022 前半 メタデータ管理・検索に DataCatalog の活用を開始

#### 話を聞きに行きたい

* Slack: [#あらマチ](https://wantedly.slack.com/archives/CR30RNFJ8), [#looker\_feedback](https://wantedly.slack.com/archives/C018LSCDR7T), [#wg-data-infrastructure](https://wantedly.slack.com/archives/C01CK0N0P6H)

#### もっと知りたい

* <https://github.com/wantedly/looker>
* <https://github.com/wantedly/analytics>
* <https://github.com/wantedly/bqv-catalog>
* <https://github.com/wantedly/bqs>


# レコメンデーション

ここでは、ウォンテッドリーのプロダクトにおける推薦システムについて紹介します。

## 推薦システムってなに？

推薦システムは、ユーザーの好みや行動傾向に基づいてどのコンテンツを提示するかを制御する技術を指し、オンラインショッピングや映画・音楽のストリーミングサービスなど、さまざまなサービスで使われています。例えば、Amazon の「お客様におすすめの商品」や Netflix の映画推薦などがイメージしやすいと思います。

[推薦システム実践入門(オライリー・ジャパン、2022年)](https://www.oreilly.co.jp/books/9784873119663/)によると、推薦システムは「複数の候補から価値のあるものを選び出し、意思決定を支援するシステム」と定義されています。複数の候補から価値のあるものを選び出すだけでなく、それをユーザーに適切に提示するところまでが推薦システムになるため、データ基盤や機械学習、バックエンド、フロントエンドなど多くの技術領域によって成り立っています。

## なぜ推薦システムが必要なのか

サービスにたくさんの魅力的なコンテンツがあっても、ユーザーがそのコンテンツに出会うことがなければ決して価値は生まれません。ユーザーが成功体験を得られるよう、次のような課題を解消するために推薦システムが必要となります。

* 膨大な選択肢: サービス内に存在するアイテムは膨大な量にのぼり、ユーザーがその中から自身のニーズに合致したものを見つけ出すことは困難である
* 自己理解の難しさ: ユーザーは自身のニーズを完全に理解できておらず、それはコンテキストや時間によっても変動する

## Wantedly Visit で使われている推薦システムの一例

* ユーザーへの募集の推薦（例：募集一覧の募集の並び順）
* 採用担当者への候補者の推薦（例：スカウト一覧の候補者の並び順）
* ユーザーへのユーザーの推薦（例：ユーザープロフィールの「あなたの知り合いかも」枠）
* ユーザーへのタイムライン投稿の推薦（例：タイムラインのフィードの並び順）
* ユーザーへの福利厚生の推薦（例：Perk 一覧の並び順）

## Wantedly Visit の推薦システムの概略

Wantedly Visit における推薦システムの概略図を以下に示します。

![](/files/oVf4fdtgcIobcAYywCwF)

## ユーザーへの募集推薦のロジック

募集一覧における推薦アーキテクチャの概略を以下に示します。

![](/files/sZXbxPWV1gcgaf7kwkAc)

この推薦は、全ての募集から候補を絞り込む「候補生成」と、候補集合を並べ替える「ランキング」の 2 段階で構成されています。 図中の「相互推薦」では、ユーザーが募集に応募する見込みと、応募後に企業側とマッチする見込みを合わせて扱います。このように双方の見込みを扱う理由は、採用マッチングが他の推薦とは異なる性質を持つためです。

## 採用マッチングと他の推薦の違い

採用マッチングは、EC の商品推薦や動画配信のコンテンツ推薦、SNS のユーザー間推薦などとは異なるいくつかの特性を持っています。これらの特性は、推薦アルゴリズムの設計だけでなく、評価指標の選び方、ログ設計、運用プロセスにも影響します。

### 両面性

採用マッチングは、求職ユーザーと採用担当者の双方にとって価値のある出会いを成立させて初めて意味を持ちます。企業側から見れば採用候補者の経験やスキル、ビジョンやカルチャーへの共感などが重要な要素になりますし、採用候補者側から見れば、その企業でやりたいことが出来るか、経験を活かせるだけでなく成長の機会があるか、労働条件は自分に合っているか、就職・転職活動を通じて新たな自分に出会えるか、なども重要な要素になります。そのため、片側のクリック率や閲覧数、応募/スカウトなどの上流の指標だけで最適化するのではなく、返信・マッチなど双方の合意を伴うより下流の指標も用いてパイプラインを設計・評価することが求められます。一部の人気募集に推薦が集中しすぎないよう対処したり、露出の偏りを観察し続けることも欠かせません。

### 意思決定の重さとインタラクションのスパース性

ECサイトや音楽・動画配信サービスなどでは、ユーザーが何度も利用する中で大量のインタラクションログが蓄積し、ユーザーの好みや嗜好を学習していくことが出来ます。一方で、採用マッチングでは、他のドメインほど頻繁にインタラクションが起こるわけではありません。これは、転職や採用が、求職者の人生や会社の業績に関わる大きな意思決定となるためです。

### フィードバックの遅延

最終的なマッチングの成功を示す指標は、ユーザーの行動ログからはすぐには観測できず、数週間から数ヶ月後にようやく観測できるようになります。一方で、長期的な指標のみを見ていると改善のサイクルが回しにくくなるため、中間指標を設定して短期的な利益に偏らないようにしつつも、中長期での利益を最大化するように推薦システムの設計や評価を工夫する必要があります。

### 流動性

推薦対象である募集には採用枠があり、採用が決まればクローズされます。求職ユーザー側にも転職活動の期間があり、活動を終えた後は推薦の対象として適切でなくなります。コンテンツ推薦のように「同じアイテムを多くのユーザーに何度でも推薦する」モデルが必ずしも当てはまらない領域だといえます。

また、新しい求職ユーザーや新しい募集が継続的に発生し、過去のログがほとんど存在しない状態の対象を扱うことも多いです。このため、行動ログが乏しい対象にも推薦できる仕組みを用意することが求められます。場合によってはデータの分布が継続的に変わることを前提として、再学習やモデル更新の運用フローを設計しておく必要があります。

### 公平性

採用は雇用機会に関わるため、属性（性別・年齢・国籍など）に基づく不当な差別が起きないよう配慮が求められます。そのため、入力特徴量や学習データに、雇用差別につながりうる属性が直接的・間接的に含まれていないかを意識して設計する必要があります。さらに、推薦結果の偏りを継続的に観測する仕組みを備え、問題が見つかった際に振り返れるよう判断根拠を残しておくことも求められます。

### Wantedly Visit の相互推薦システム開発の取り組み

このような背景から ウォンテッドリーでは相互推薦システムの開発に注力しています。詳しくは以下のブログにも書かれているので、ぜひご覧ください。

* [相互推薦システムを活用したユーザーと企業の双方の嗜好を考慮した推薦](https://www.wantedly.com/companies/wantedly/post_articles/903172)
* [Wantedly Visit における相互推薦システムの活用事例](https://speakerdeck.com/chimuichimu/wantedly-visit-niokeruxiang-hu-tui-jian-sisutemunohuo-yong-shi-li)
* [マッチング推薦におけるオフ方策評価・学習](https://www.wantedly.com/companies/wantedly/post_articles/1015424)
* [相互推薦の集約をマルチタスク学習でパーソナライズする](https://www.wantedly.com/companies/wantedly/post_articles/1041103)

## ウォンテッドリーの推薦チームは何に取り組んでいるのか

ウォンテッドリーの推薦チームは、「人と会社のよりよい出会いを、データと機械学習で支えるチーム」ですが、取り組みの範囲は機械学習モデルの精度向上だけにとどまりません。どの画面で・どのタイミングで・どう見せるか、リリース前後の安全性とユーザー体験の検証までを扱います。推薦を単なるアルゴリズムではなく、ユーザーの意思決定を助けるプロダクト体験として捉えている点が特徴です。

### 推薦チームの仕事の流れ

推薦チームは、問題発見・原因分析・解決策の立案・評価・リリース・運用までを一気通貫で扱います。

まずは問題の特定です。例えばある画面で募集が表示されているのに反応が薄い場合、推薦精度の問題以外にも、特定のセグメントに対する推薦内容が合っていない、推薦タイミングが適切でないなど、複数の原因が考えられます。

次に仮説を立てます。行動ログから離脱画面や閲覧傾向を集計したり、過去施策の振り返り、簡易的な事前検証などをもとに、なるべく妥当性の高い仮説を立てていきます。

仮説が立ったら解決策を考えます。既存のアルゴリズムの改善や新しいランキングの導入、前処理・後処理の追加などを検討します。

実装した変更はすぐに全ユーザーへ出さず、まず評価します。「このユーザーにはこの募集が正解」と事前に決めるのが難しいため、評価設計が特に重要です。過去ログによるオフライン評価、人による定性評価、ユーザーテスト、本番環境でのオンラインテストを組み合わせます。

オンラインテストでは A/B テストや Interleaving を用います。A/B テストはユーザーをグループに分けて従来/新規の推薦を比較する方法、Interleaving は複数の推薦結果を混ぜて同じ画面に出し、効率よく比較する方法です。オンラインテストでの評価が良好であれば、正式にリリースします。

新しいプロダクト機能に対して推薦ロジックを導入する際は、他チームのエンジニアと協業して、推薦のためのデータ収集やログ・APIの設計、画面への表示方法などを検討します。リリース後は、推薦結果の偏りやユーザー行動の変化をモニタリングし、必要に応じて改善を行います。

### 実験を速く、正しく回すための仕組み

推薦チームが大事にしているのは実験主義です。最終的な答えはユーザーの反応からしか分からないという前提に立ち、仮説を言語化し、必要最小限の実験で結果から学び、次の改善につなげる姿勢を指します。

実現には、実験を速く回せることと結果が信頼できることの両立が必要です。速くても評価が雑だと誤った学びを得てしまい、評価が厳密でも準備に時間がかかれば改善スピードが落ちます。

そのため ウォンテッドリーではオフライン評価用の内製ライブラリを整備しています。以前は施策ごとに評価コードを個別実装していましたが、現在は共通処理をまとめ、設定ファイルで評価内容を指定できるようにして準備の負担を下げています。

また、モデル出力だけでなく、実際にユーザーが見る推薦結果に近い形を再現して評価することも重視しています。モデルの並び順は検索条件・フィルタ・後処理を経て画面に表示されるため、モデル単体では良くてもユーザーに届く形では期待と違うことがあります。そこでユーザーが見る状態を可能な限り再現し、オフラインとオンライン評価のずれを小さくする工夫をしています。

デプロイのプロセスも改善されています。以前はデータサイエンティストの実装後に推薦基盤エンジニアが推論サーバー側の実装を行う必要があり、リードタイムが長くなりがちでしたが、現在はランキングの条件分岐やテスト形式をスキーマファイルに書くと必要なプログラムが自動生成される仕組みが整っているため、データサイエンティスト単独でも安全にデプロイしやすくなっています。

### MLOps の取り組み

推薦システムは作って終わりではありません。ユーザー行動・募集内容・企業の活動・画面構成は日々変わり、過去にうまく機能していたモデルも時間が経つと性能が落ちます。機械学習モデルを安定して開発・運用するための仕組みが MLOps で、開発・デプロイ・監視・運用の仕組み全体を指します。

推薦チームでは MLOps を「特徴量管理」「モデル学習と評価」「デプロイとサービング」「モニタリングと運用」の観点で整備しています。

**特徴量管理**: 特徴量は機械学習モデルの判断材料となるデータ(過去に見た募集、スキル、業種、職種、過去の接点など)です。複数の推薦システムで再利用できるよう Feature Store を内製しています。

**モデル学習と評価**: 多くの推薦モデルはバッチジョブとして自動学習されており、人手をかけずに新しいデータを反映した推薦を提供できます。

**デプロイとサービング**: サービングでは、新しいロジックを素早くオンライン検証してからリリースする流れが整っています。現在はバッチ推論が中心ですが、ユーザーの現在の状況を即時に反映するオンライン推論への移行が課題です。

**モニタリングと運用**: モデルの性能指標とビジネス指標を定期的に確認する文化があります。入力データの分布が変わるドリフトも課題で、ユーザー行動や募集内容が変わると過去データで学習したモデルの前提が崩れ精度が落ちます。また、ドリフト以外にも様々な要因でランキングが想定通りに表示されないことがあり、そのような異常にすぐに気付いて対処出来るよう、アラート通知やログの可視化などの仕組みを整えています。

### 今後取り組もうとしている課題

推薦チームは、これまで Wantedly Visit における推薦を中心に開発してきましたが、今後は Wantedly Visit の推薦をさらに深めることと、そこで培った技術を他プロダクトや業務にも広げることに取り組みます。

* **最適挑戦につながる推薦**: 簡単すぎず難しすぎない、その人の成長や前向きな変化につながる仕事との出会いを実現するため、スキル・性格・志向性・仕事上の特性をより深く理解する。
* **非構造データの活用**: 表形式に整理しづらいテキスト・画像・つながり情報などを、自然言語処理や深層学習で理解し、ユーザーの価値観や企業の魅力を推薦に活かす。
* **LLM と推薦の組み合わせ**: 大規模言語モデルを用いて、例えば自由記述プロフィールからのスキル抽出、リランキング、生成的推薦などに取り組む。
* **生成 AI によるコンテンツ作成支援**: マッチングの出発点となるプロフィールや募集文の作成負担を下げ、より多くの人や企業が魅力を伝えやすくする。
* **Wantedly Visit 以外のプロダクトへの展開**: Wantedly Hire や Perk など、他のプロダクトでの推薦・検索体験に Wantedly Visit の知見を横展開する。
* **社内業務の最適化**: 営業・マーケティング・スパム検知・コンテンツモデレーション・社内ナレッジ活用などに応用し、組織全体の生産性向上に貢献する。

### 話を聞きに行きたい

* Slack: [#visit\_recommendation (internal)](https://wantedly.slack.com/archives/CA0PVDX98)

### もっと知りたい

* <https://github.com/wantedly/visit-recommendation> (internal)
* [ウォンテッドリーにおける推薦システム開発の流れ (Wantedly Engineer Blog)](https://www.wantedly.com/companies/wantedly/post_articles/864502)
* [相互推薦システムを活用したユーザーと企業の双方の嗜好を考慮した推薦 (Wantedly Engineer Blog)](https://www.wantedly.com/companies/wantedly/post_articles/903172)
* [Feature Storeを活用して最新のデータを学習時に取り込むことによる推薦システムの改善 (Wantedly Engineer Blog)](https://www.wantedly.com/companies/wantedly/post_articles/897815)
* [ウォンテッドリーのデータサイエンティストが今後取り組む主要課題 (Wantedly Engineer Blog)](https://www.wantedly.com/companies/wantedly/post_articles/973103)
* [ウォンテッドリーの推薦システムを支えるMLOps -現状の取り組みと課題- (Wantedly Engineer Blog)](https://www.wantedly.com/companies/wantedly/post_articles/1047307)


# Looker 入門

ここでは、ウォンテッドリーでデータを活用して可視化するために導入されているツールである Looker の概要について解説します。 Looker の開発に必要となる前提や手順の説明に限定し、開発と関連しない Looker の機能やその運用方法については扱いません。 なお、説明は概要にとどめて細かい実装の詳細などは他のドキュメントで補完していくものとします。

## 想定読者

主に下のような人を想定しています。

* Looker で集計・可視化できるデータを追加・変更したい人

## Looker の概要

### Looker とは

Looker は、データを集計・可視化することで、より良いビジネス上の意思決定を行えるようにするための BI ツールです。

以下に Looker の代表的な機能と、各機能の概要および手順を把握するためのドキュメントを示します。 Looker にはじめて触れる場合は、これらのドキュメントを参照して概要を把握すると良いでしょう。

* コンテンツ（ダッシュボードや Look）を検索・閲覧する
  * [Find and organize content](https://docs.looker.com/sharing-and-publishing)
* データを集計・可視化する
  * [Retrieve and chart data](https://docs.looker.com/exploring-data/retrieve-chart-intro)
* コンテンツ（ダッシュボードや Look）を作成・編集する
  * [Create and edit dashboards and reports](https://docs.looker.com/dashboards/dashboards-reports-intro)
* 開発する
  * [Get ready for development](https://docs.looker.com/data-modeling)

以降は、開発の機能に関連する内容に限定して説明を続けます。

### Looker の特徴

Looker の最大の特徴は、データモデリングによってデータの定義を管理することにあります。

適切な粒度でデータをモデル化することで、データの定義が再利用可能になり、以下の恩恵を得ることができます。

* 類似する定義の乱立を避け、定義の一元管理が可能になる
* 定義が利用箇所ごとに異なることを避け、同一の定義でデータを扱うことができる

また、Looker はデータに関する定義の管理をする際に、Git を利用したバージョン管理や、GitHub と連携したコラボレーションをすることを前提としています。 これらの仕組みによって、効果的、かつ、効率的にデータの定義を管理できることも、Looker の特徴のひとつと言えるでしょう。

### LookML プロジェクト

Looker におけるデータに関する定義は、LookML プロジェクトで管理されます。 LookML プロジェクトとは、モデルとビューを定義するファイル群の総称です。

モデルとビューのファイルには、それぞれ以下の情報を含みます。

* モデルのファイル
  * 利用するビュー、および、ビュー同士がどのように結合されるかの情報
* ビューのファイル
  * 参照するテーブル、および、テーブルからどのように情報を抽出・集計するかの情報

Looker で集計・可視化できるデータを追加変更する場合、これらのファイルを変更することが必要になります。

### Explore と LookML プロジェクト

Explore は、データを効率的に探索することができる Looker の強力な機能です。

Explore では、様々なデータの集計・可視化を行なうことができます。 Explore の利用方法はドキュメントを参照してください。 [Exploring data in Looker](https://docs.looker.com/exploring-data/exploring-data)

LookML プロジェクトにおけるモデルのファイルには、この Explore を定義します。 また、ビューのファイルには、Explore におけるデータの抽出・集計のために利用されるディメンションやメジャーを定義します。

以下はよくあるユースケースと、それを実現するために必要となる変更対象のファイルと変更内容です。

* 新たなビューを利用してデータを集計・可視化する
  * 変更対象：モデルのファイル
  * 変更内容：Explore の定義を追加・変更する
* 新たな抽出・集計の方法でデータを集計・可視化する
  * 変更対象：ビューのファイル
  * 変更内容：ディメンションやメジャーの定義を追加・変更する

これらの整理を参考に、実現したいユースケースに対して何を変更すべきであるかを理解してください。

ここまで説明した LookML プロジェクトや関連する用語についての詳細はドキュメントを参照してください。 [LookML terms and concepts](https://docs.looker.com/data-modeling/learning-lookml/lookml-terms-and-concepts)

## 開発の流れ

LookerML プロジェクトの変更は、Looker が提供する IDE で行ないます。 まずは、サイドバーから、"Develop" > "wantedly" の順に進み、LookML プロジェクトを開きましょう。

もしくは、下記のリンクから直接 LookML プロジェクトを開くこともできます。 [wantedly プロジェクト (internal)](https://wantedly.jp.looker.com/projects/wantedly)

### 開発権限の申請

LookML プロジェクトの変更を行なう場合、開発権限が必要になります。

Looker の開発には誰でも参加できます。 開発権限がない場合は[開発者権限の申請 (internal)](https://dev-docs.wantedly.com/looker/request-developer-role)を参考に申請を行ない、開発に参加しましょう。

### 開発モードの有効化

LookML プロジェクトに変更を加えるには、開発モード (Development Mode) が有効になっている必要があります。 画面上部に青色のバナーが表示されているのが、開発モードであることの目印です。

開発モードの有効化の方法はドキュメントを参照してください。 [Switching in and out of Development Mode with enhanced navigation](https://docs.looker.com/data-modeling/getting-started/dev-mode-prod-mode#switching_in_and_out_of_development_mode_with_enhanced_navigation)

### ブランチの作成

Looker は Git を利用したバージョン管理を行ないます。 開発用のブランチの作成を行ない、LookML プロジェクトに変更を加える準備をしましょう。

開発用のブランチの作成方法はドキュメントを参照してください。 [Working with Git branches](https://docs.looker.com/data-modeling/getting-started/version-control-and-deploying-changes#working_with_git_branches)

### LookML プロジェクトの変更

Looker の IDE で、LookML プロジェクトの変更を行ないます。 モデルとビューのファイルの追加や変更を行ないます。

詳しい手順は、後述のデータモデリング手順を参考にしてください。

### Explore を利用した動作確認

開発モードでは、LookML プロジェクトの変更内容を反映した結果を確認することができます。 Explore を用いて、期待するデータの抽出や集計ができるかを確認するのが一般的です。

### GitHub を利用したコードレビュー

Looker は、GitHub と連携しているので、Looker の IDE からコミットなどの Git の操作だけでなく、GitHub の PR を作成することも可能です。 変更を終えたら適切な粒度でコミットし、GitHub の PR を作成してください。

LookMLで不明瞭な点があれば @wantedly/bi をレビュアーに含めてください。 それ以外は各チームのドメイン知識のあるメンバーをレビュアーにしてください。

あとは、他の開発フローと同様に、コードレビューと指摘内容に対する修正を終え、開発用のブランチをマージしてください。

### 開発と並行したダッシュボード作成時の注意点

他の利用者が共有されたコンテンツ（ダッシュボードや Look）を閲覧する場合は、開発モードの影響を受けず、常に本番モード (Production Mode) の LookML プロジェクトを参照していることに注意してください。 開発モードにおける LookML プロジェクトの変更を前提としたコンテンツの追加や変更を行なうと、他の利用者からはコンテンツでエラーが生じて内容を閲覧できないということが発生してしまいます。

他の利用者と共有されるコンテンツの追加や変更をする際は、必ず開発用のブランチをマージしてから行なうようにしましょう。

## データモデリング手順

ここでは、スタースキーマを作成することを前提としてデータモデリング手順を説明します。

スタースキーマとは、データモデリングの代表的な手法のひとつです。 スタースキーマでは、ファクトテーブルとディメンションテーブルを組み合わせます。 各テーブルは以下のようなテーブルを指します。

* ファクトテーブル
  * 関心対象となるイベントの発生ごとに1レコードで表現したデータ
* ディメンションテーブル
  * 人や場所、物などの属性値を表現するデータ

以降は、具体例として、スカウトのファクトテーブルと、ユーザーのディメンションテーブルを用いて、スタースキーマを作成することを想定して説明を続けます。

### ビューの定義

テーブルを参照できるように、ビューのファイルの作成と定義をすることから始めます。

ビューのファイルを新規で作成する最も簡単な方法は、テーブルの情報からビューを作成する方法です。 ビューのファイルを追加したいフォルダのメニュー > "Create View From Table" を選択し、テーブルを選択してビューのファイルを作成することができます。

詳しいビューのファイルの作成方法はドキュメントを参照してください。 [Adding a new view from an existing database table](https://docs.looker.com/data-modeling/getting-started/model-development#adding_a_new_view_from_an_existing_database_table)

作成されるビューのファイルには、参照するテーブルの指定、テーブルのすべてのカラムに対応するディメンションなどが含まれる LookML の記述が含まれます。

以下は、スカウトのテーブルを参照するファクトテーブルを作成した場合の例です。

```lookml
view: scouts {
  sql_table_name: `project.dataset.scouts` ;;
  drill_fields: [id]

  dimension: id {
    primary_key: yes
    type: number
    sql: ${TABLE}.id ;;
  }

  dimension: user_id {
    type: number
    sql: ${TABLE}.user_id ;;
  }
  ...

  measure: count {
    type: count
    drill_fields: [id]
  }
}
```

作成されたビューに対してディメンションを追加するには [dimension パラメータ](https://docs.looker.com/reference/field-params/dimension)を、メジャーを追加するには [measure パラメータ](https://docs.looker.com/reference/field-params/measure)を用いて宣言を記述します。

ビューの定義を追加する際に利用できるすべてのパラメータについてはドキュメントを参照してください。 [View parameters](https://docs.looker.com/reference/view-reference)

### Explore の定義

次に、定義したビューを用いて Explore の定義を行ないます。

モデルのファイルに新しく Explore の定義を追加するには、[explore パラメータ](https://docs.looker.com/reference/explore-params/explore)を用いて宣言を記述します。 多くの場合、Explore で利用するビューと同名の explore を追加します。 スタースキーマを作成することを前提とする場合は、ファクトテーブルを参照するビューと同名の explore を追加すると良いでしょう。

また、explore パラメータ内では、Explore の表示やフィルタの動作などを様々なパラメータを用いて指定することができます。 多くの場合、Explore で結合されるビューを [join パラメータ](https://docs.looker.com/reference/explore-params/join)を用いて指定します。 スタースキーマを作成することを前提とする場合は、ディメンションテーブルを参照するビューを結合されるビューとして指定すると良いでしょう。

以下は、スカウトのファクトテーブルを参照するビューと同名の explore を追加する例です。 結合されるビューとして、ユーザーのディメンションテーブルを参照するビューを指定します。

```lookml
explore: scouts {
  join: users {
    relationship: many_to_one
    sql_on: ${scouts.user_id} = ${users.id}
  }
}
```

最後に、作成したexploreをmodels ディレクトリ配下でモデルを定義するファイルを追加します。 `<ドメイン名>.model.lkml`ファイル内に以下のようにして、作成したexploreをincludeします。

```
include: "/explores/<ドメイン名>/<追加するexplore名>.explore.lkml"
```

Explore の定義を追加する際に利用できるすべてのパラメータについてはドキュメントを参照してください。 [Explore parameters](https://docs.looker.com/reference/explore-reference)

### ダッシュボード

Looker では、Explore をもとにグラフなど、視覚的なレポートを作成し、それらをダッシュボードという形でまとめることができます。

また、定期的にダッシュボードのスナップショットを取得し、Slack に通知することもできます。詳しくは [Slack 統合への配信のスケジューリング](https://cloud.google.com/looker/docs/scheduling-slack?hl=ja) を参照してください。

#### もっと知りたい

* [Looker documentation](https://docs.looker.com)
* [Looker Connect](https://connect.looker.com/)
* [Write LookML](https://docs.looker.com/data-modeling/learning-lookml/lookml-intro)


# 推薦システムの開発に使っているツール

推薦チームでは、モデルの開発・改善や、デプロイを簡単に行うための様々なツールを用意しています。以下はそのなかでも自分たちで内製しているツールの一覧です。

* 機械学習モデルの開発に使うツール
  * [feature store (internal)](https://github.com/wantedly/feature-store)
  * [recomx (internal)](https://github.com/wantedly/recomx)
  * [recommendation-evaluator (internal)](https://github.com/wantedly/recommendation-evaluator)
  * [SARA (internal)](https://github.com/wantedly/sara)
  * [servicex-python (internal)](https://github.com/wantedly/servicex-python)
* 機械学習モデルのリリースに使うツール
  * [visit-recommendation-project (internal)](https://github.com/wantedly/visit-recommendation-project)
  * [visit-recommendation-scout (internal)](https://github.com/wantedly/visit-recommendation-scout)

これらのツールと後述する外部ツールの全体像は以下の通りです。

![推薦システムの開発に使っているツールの全体像](/files/cMmxg2rFmDomwSfjLzId)

## 機械学習モデルの開発に使うツール

### feature store

モデルの学習に使う特徴量を定期的に計算し、それをBigQuery上にエクスポートして管理しています。

[参考資料](https://speakerdeck.com/zerebom/wantedlydefeature-storewodao-ru-suruji-nikao-etakoto)

### recomx

モデルの学習パイプラインの中の共通部分をまとめたライブラリです。BQ / GCS とのデータのやり取りなど、どの施策でも共通して利用出来る処理をまとめています。

### recommendation-evaluator

オンラインテストに出す前のランキングの性能を、過去データを使って低コストでシミュレーションするツールです。カスタマイズして様々な指標を簡単に実装することも可能で、施策でやりたいことを実現できそうなのかを評価することが可能です。

### SARA (Seamless Accessible Resource Allocator)

SARA は Kubernetes の サンドボックスクラスタ内に指定のリソースで頻繁に必要になるアセット(PVC や Claude Codeなど) をデフォルトで備えた Pod を起動・管理するためのツールです。このツールにより、ローカルのPCに大量のコンピューターリソースが存在するかのような開発体験を提供することができ、エンジニアは本番運用時とほとんど同じ環境で開発を行うことができます。

### servicex-python

マイクロサービス共通ライブラリである servicex の Python 実装です。servicex については [マイクロサービス共通ライブラリ "servicex" の紹介](/fields/the-system/servicex) を参照してください。

## 機械学習モデルのリリースに使うツール

### visit-recommendation-project

Wantedly Visit の募集の集合からクエリに対して適切なフィルタリング・オーダリングを行ってユーザーに返すサービスです。作成したランキングを本番環境にリリースしたり、オンラインテストを行ったりする際に利用します。

### visit-recommendation-scout

Wantedly Visit上にいるユーザーの集合からクエリに対して適切なフィルタリング・オーダリングを行ってリクルーターに返すサービスです。作成したランキングを本番環境にリリースしたり、オンラインテストを行ったりする際に利用します。

上記2つについては[WantedlyのPodcast](https://open.spotify.com/episode/3F24EoFf3ImNwijrpU7ggB?si=dh5GxSDbT1a4A5QGA4cuCA)や[発表資料](https://speakerdeck.com/hazumirr/accelerate-improvement-measures-for-recommendations?slide=20)でも触れられています。ランキングの条件分岐やテスト形式をスキーマファイルに書くと必要なプログラムが自動生成される仕組みが整っているため、データサイエンティスト単独でも安全にデプロイしやすくなっています。

## その他の外部ツール

上に挙げた以外にも、推薦チームでは開発時に様々なツールを使用しています。

* Argo workflow
* BigQuery
* Looker
* Grafana

## 話を聞きに行きたい

* Slack: [#visit\_recommendation (internal)](https://wantedly.slack.com/archives/CA0PVDX98)


# 開発プロセス


# Git の慣習

[技術とアーキテクチャ](/introduction/technical-overview) で解説されている通り、Wantedly のサービスは、各々が独自のリリース・サイクルを持つ多数のソフトウェア・コンポーネントの組み合わせによって動作しています。 それぞれにコンポーネントに対して 1 つの repository を管理しています。

## 一般の慣習

このセクションでは技術領域に関わらない慣習を解説します。

### main branch の利用

2025年12月から、ウォンテッドリーではデフォルトブランチ名として main の利用を推奨する方針となりました。それ以前はほとんどのリポジトリで master を使っていました。

デフォルト設定として以下を推奨します。

```sh
# git init のデフォルトを変更 (Git 3.0 以降では不要になる予定)
git config --global init.defaultBranch main
```

インフラ側ではブランチ名の変更について基本的な対応はできています。2025年12月時点で、いくつかのリポジトリはすでに `main` branch で問題なく運用が続けられています。

2025年12月時点では `master` branch のままになっているリポジトリが多数派です。これらの既存リポジトリについても、 `main` への変更を漸進的に行う予定です。

### branch 名に username を入れる

GitHub Flow を採用している背景から、殆どの場合一つの branch と一人の開発者がひも付きます。 不要なコンフリクトを避けるために branch は `<github-username>/<feature-name>` のように命名します。

### Pull Request 重視

ウォンテッドリーでは伝統的に Pull Request の description をコミットメッセージよりも重視します。 コミットメッセージで深く悩まずにコミットを重ねて早めに Pull Request を作ると良いでしょう。 Pull Request の書き方は [Pull Request の書き方](/fields/dev-process/how-to-write-a-pull-request) を参照してください。

## マイクロサービス開発における Git

このセクションではマイクロサービスの repository 特有の慣習について解説します。 バックエンド及びフロントエンドのコンポーネントがこれに該当し、 モバイルアプリ / 社内ライブラリ / 社内ツールなどは必ずしもこのセクションで解説する慣習をとっていないことがあります。

### Kubernetes / Docker との対応

マイクロサービスは下の性質を持ちます。

* Kubernetes クラスタに Deploy される
* 1 つの GitHub repository をもつ
* 1 つの Kubernetes namespace (repository 名と同名)をもつ
* 1 つの Docker image をもつ

このため一つの名前で、GitHub repository, Kubernetes namespace, 一つの microservice など複数のものを指すことがありますが、 上記の対応関係のために混乱することは殆どありません。

Kubernetes 概要およびウォンテッドリーにおける運用については [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction) を参照してください。

### GitHub Flow の採用

バックエンド及びフロントエンドでは [GitHub Flow](https://githubflow.github.io/) を採用しています。 したがって下のようなスタイルの開発になっています。

* master branch から topic branch を切る
* master branch に対して pull request を作る
* master branch が更新されるたびに本番環境に反映される

これを維持するために commit を push するたびに Docker image が作成され、master branch の更新に合わせて自動で本番環境が更新されるようになっています。

技術的な詳細は [リリース・デプロイ戦略を支える技術](/fields/infrastructure/deploy-strategy-implement) を参照してください。


# Pull Request の書き方

## はじめに

ウォンテッドリーでは書いたコードについて Pull Request を通じてレビューを受けることが必須になっています。 コードレビューは仕事を前に進めるためには避けては通れないという事実がある一方で、コードレビューで多くの時間を使ってしまっているケースや完全に作業が止まってしまっているケースを見ることがままあります。

コードレビューはレビュイー(reviewee)からレビュアー(reviewer)への仕事の依頼です。 レビュアーがパフォーマンス高く効率的にレビューができるか否かは仕事を依頼するレビュイーの段取りにかかっています。

本記事では Pull Request を作る際に心がけるべきポイントを紹介します。

## 小さい単位で Pull Request を作る

可能な限り小さい機能単位で Pull Request を作ったほうが素早く質の高いレビューを受けられることが多いです。 適切なまとまりを考えて、小さい Pull Request を出していくと良いでしょう。

なぜ小さい Pull Request を作るべきかについては Google のエンジニアリングプラクティスのページが非常にわかりやすく説明しているので、こちらも合わせて読んでみてください。 <https://google.github.io/eng-practices/review/developer/small-cls.html>

## Why - Pull Request がなぜ必要なのかを辿れるようにする

経緯を全ての人が知っているとは限らないので Why の部分は必ず書きましょう。issue のリンクを貼っておくことで十分な場合もあります。後でこの PR を見た人が Why の部分を理解できるようにしておくことが重要です。 (それはレビュアーかもしれないし、後にバグを直す人かもしれないし、半年後の自分かもしれない)

![](/files/-MhWAOIacwjlo_fx6V_1)

## 一連の作業の中での位置づけを示す

小さい単位で Pull Request を作るべきということを書きましたが、そうなるとある機能を実装するためにいくつかの PR を分けて作成することが多くあります。 そのときに、依存関係がある一連の作業におけるこの PR の位置づけを示してあげると、レビュワーが文脈を理解しやすくなります。issue の側でタスクリストを作成し、「タスクの全体像は issue XXX を参照」のような書き方をすることもよくあります。

これは必ずしもすべての PR で行う必要はありませんが、テクニックとして覚えておくとよいでしょう。

![](/files/-MhWAOIdbOxOiuycd3aZ)

![](/files/-MhWAOIecVWYaCNN4IAQ)

## What - 変更の内容を簡潔に説明する

What にはその PR で行った変更の内容を簡潔に記述してください。ここではコードの変更内容をただ列挙しただけのようなものは求めていません。それよりも、もう少し抽象的なレベルでどんな変更を行ったのかを記述するようにしてください。特に AI に PR の description を書かせると、よくない例のようになりがちなので気をつけてください。

よい例：

> /demo\_menu というパスでデモメニュー画面を追加し、そこからオンボーディングデモの起動が行えるようにした。\
> また、ホーム画面のコンテンツの最下部にデモメニューへの導線を追加した。
>
> デモメニューは本番環境以外でのみアクセスできる。

よくない例：

> * /demo\_menu のルーティングを追加
> * DemoMenuController に index アクションを追加
> * XXX に YYY を追加
> * ...

また、画面の変更を行った場合はスクショや動画を貼るようにしてください。そうすることで、レビュワーがどんな変更が行われたのかを理解しやすくなります。変更前と変更後の両方のスクショを貼るとよりわかりやすくなることもあります。

## 何を動作確認したのかを記載する

まず前提として、行った変更が動作することを確認するのはレビュワーではなく、PR を作る側の仕事です。レビュワーが「この変更はそもそもちゃんと動いているのだろうか」と疑問を持ちながらレビューをすることにならないよう、どのような動作確認を行ったのかを記述してください。

画面で操作する機能の場合は、動作確認をした動画を貼るのが一番いいです。一方、純粋なバックエンドのみの実装の場合は、適切なテストが書かれていれば、動作確認の記述は必ずしも必要ではありません。

![](/files/uj5I56izQennIW5u1kCY)

## レビューで確認してほしい観点を記載する

これも必ずしも毎回やる必要はありませんが、こういう観点でレビューしてほしいというものがあれば、それを書いておきましょう。

例：

* クラスの責務の切り方（XX に YY を持たせている点）が妥当か
* 実装方法に自信がないので、もっといい書き方があれば知りたい

また、設計自体に不安があればコードを書き始める前にレビュアー(となる予定の人)と適切にコミュニケーションを取って事前に設計方針を固めておきましょう。 ほとんどコードを書いてしまってからレビューの段階で設計自体の問題点を指摘された、かつそれがコードを書く前でも問題を指摘可能だった場合に大きく無駄を生んでしまいます。

## 適宜レビューがしやすくなるような補足コメントを入れる

What には変更のサマリーを記載しますが、適宜個別の箇所にも補足説明を入れると、レビューがよりしやすくなることがあります。

![](/files/5iRg85lR8wQ7FFxpOylz)

![](/files/PvTQMHMMD2ie0AnrhnJd)

ただし、その情報が PR のレビュワーだけでなく将来の開発者にとっても有益な情報なのであれば、それは PR 上でコメントを入れるのではなく、コードに直接コメントで書くようにしましょう。例えば何か理由があって特別な処理をするコードを書いた場合には、そのような処理を行っている理由をコード上のコメントに残しておくべきです。

## AI レビューの指摘は resolve しておく

ウォンテッドリーでは PR を作成すると Devin Review などのAIレビューが自動で走るようになっています。AIレビューはコードの不具合などを精度高く指摘してくれ、今や開発プロセスに欠かせない存在になっています。

AIレビューの指摘については、チームメンバーにレビューを依頼する前に、以下を行っておきましょう。

* まず本当に対応する必要があるかを判断する
* 対応する必要があると判断した場合には、対応して、指摘コメントを resolve する（多くの場合、修正 commit を積むとAIが修正されたことを検知して勝手に resolve してくれます）
* 対応する必要がないと判断した場合には、そう判断した理由（実際には〜〜なのでその問題は発生しない、かなりレアケースなのでそこまではやらなくてよいと思った、など）をコメントしてから resolve する
  * 妥当な指摘に対して無言 resolve はしないようにしましょう。

#### もっと知りたい

* [コードレビューの際に気をつけること - Qiita](https://qiita.com/awakia/items/8344ba751426e386e0f5)
* [The CL author’s guide to getting through code review | eng-practices](https://google.github.io/eng-practices/review/developer/)


# ポストモーテムの取り組み

## TL;DR

* ポストモーテムは、Incident の事例を振り返り、改善のために学んでいく取り組みです。
* Infra と各チームの SRE が中心になって、 ポストモーテムの作成や同期的なレビュー会を毎週行っています。
* <https://github.com/wantedly/post-mortems> (internal) に過去のポストモーテムをまとめています。

## はじめに

ウォンテッドリーでは、日々新しい機能や新しいシステムが追加されています。 そのため、成長と同時に複雑な分散システムになりつつあります。 加えて、外部環境も変化しています。サービスが大きくなるほどにユーザーの行動も多様化し、様々なアクセス元からの危険なリクエストも日々やってくるようになります。

こうした条件の下では、インシデントやサービス障害を未然に防ぐためのコストは、要求する水準に対して指数的に(急激に)大きくなります。 もちろん完璧なサービスを提供できるならそれに越したことはないですが、私たちの目的はプロダクトを通じて社会に影響を与えることであり、完璧なサービスを作ることではありません。かけられるコストには限界がある以上、インシデントが起きうること自体は許容せざるを得ません。

しかし、こういったインシデントから学びを得るための定式化されたプロセスがなければ、同じようなインシデントが無限に繰り返し起こることになります。また野放しのままになってしまえば、インシデントの複雑さは加速度的に増し、あるいは積み重なってシステムの対処ができなくなります。

そのため、インシデントが発生し、対処したあとは、その一部始終から学ぶために振り返りのポストモーテムを書きます。

## ポストモーテムとは？

> ポストモーテムは、インシデントとそのインパクト、その緩和や解消のために行われたアクション、根本原因（群）、インシデントの再発を避けるためのフォローアップのアクションを記録するために書かれるものです。
>
> 抜粋:: Betsy Beyer “SRE サイトリライアビリティエンジニアリング”

ポストモーテムの目的は、インシデントがドキュメント化されること、影響を及ばしたすべての根本原因（群）が十分に理解されること、そして、再発の可能性や影響を削減するための効果的な予防策が確実に導入されるようにすることです。ポストモーテムには次のような事が書かれます。

* 発生したこととその影響範囲
* その緩和や解消のために行われたアクション
* 根本原因
* 再発防止策

ポストモーテムを書くはのは処罰ではなく、会社全体としての学びの機会です。サービスのどの部分をどうすれば改善できるのかを提起し、長期的な成長を鈍化させないプロセスです。

|     -     |  ポストモーテム |    障害報告書   |
| :-------: | :------: | :--------: |
|  想定読者は誰か  |  社内　　　　　 |   ユーザー 　   |
| 何のために書くのか | 失敗から学ぶため | 説明責任を果たすため |

## Post mortems / Incident review での共有

知見として溜まったものを学びの機会として共有するために、週次で Post mortems / Incident review を行い、 作成したポストモーテムの共有と、内容の精査、Next Action の決定などを行っています。

参加メンバーは、Infra Squad のメンバーと、各 Domain の SRE チームのメンバーです。 加えて、その日に扱う予定の post mortems が決まっている場合、それに関係するメンバー (障害発生時に対応してくれたメンバーなど) を呼ぶこともあります。

近頃は incident と言えるような問題が起きていない週も多くなってきたため、開催頻度は低くなりつつあります。

👉 [過去に開催した Post mortems / Incident review](https://github.com/wantedly/post-mortems/labels/Post%20mortem%2FIncident%20Review)

## ポストモーテムの書き方

ウォンテッドリーでは、学びを得て蓄積していけるようなポストモーテムを書いていけるように、 「ポストモーテムを書く会」として同期的にポストモーテムを書く場を設け、そこで書いたポストモーテムをレビューしています。

最近では以下の流れで進めることが多いです。これらの流れについて説明します。

1. インシデント対応を行った関係者の間で、ポストモーテムにするべき学びがあるかどうかを判断する。
2. 一人がポストモーテムの叩き台を作る。
3. 関係者を集めてポストモーテムを書く会を実施する。
4. 書いたポストモーテムを Post mortems / Incident review 会で共有、レビューする。ここでインシデント関係者以外にも学びが共有される。

### 1. ポストモーテム化の判断

インシデントからどのような学びが得られるかについて、関係者の間で話し合います。これには大きく2つの要因が関与します。

* インシデント対応中にすでにある程度のことがわかっていて、それが共有するべき学びに相当する場合。 たとえば、原因となったデプロイが特定されていて、そのデプロイがなぜダメだったのかの目星がすでについている、というような場合です。
* インシデント対応中にはわからなかったことがあり、そこを明らかにすることが今後のインシデント対策に寄与することが期待される場合。たとえば特定のノードが異常状態になったという直接の原因はわかっているが、その根本原因を明らかにしなければ再発するリスクがある場合などです。

インシデントを振り返って、ポストモーテム化するほどの知見がない場合は対応コストとの兼ね合いでポストモーテムを作らないこともあります。ただし、「post-mortem にするほどの学びのない incident」が増えている場合は要注意です。その場合、 incident から学ぶべき上位の学び (組織課題など) がないか点検するべきかもしれません。

### 2. 叩き台の作成

ポストモーテム作成作業のうち、基本的な事実関係の収集などは一人で行ったほうが効率がよく、全員を集める必要がありません。そこで、最近ではインシデントに対応した人のうち誰か一人が叩き台を作ることが多いです。

次の「3. ポストモーテムを書く会の実施」の進め方が執筆の参考になります。

### 3. ポストモーテムを書く会の実施

事前に決めたメンバーで、同期的に Google Docs 上のドキュメントの項目を埋めていく形で進めます。 すでに叩き台がある場合は、それをベースに編集・コメントして内容を改善していきます。 会を進める上では、以下の流れに沿って KPT + Fact を意識して進めると、良いポストモーテムが書きやすいです。

* まず Fact を整理して、参加しているメンバーが当時の状況を把握できている状態にする。
  * Incident の原因、ユーザー影響、復旧要因、タイムラインに関する項目を埋めていく。
* それらから Keep, Problem として、継続していきたい内容、再発防止したい内容、今後改善していきたい内容を洗い出す。
  * テンプレートの Lessons Learned を使うと洗い出しやすいです。
* Problem を改善するための Try を書いていく。
  * TODO, Long-term Action として書いていく。
  * 問題が大きい場合は、以下のように色々な観点で問題を分解すると進めやすいです。
    * 未然に防ぐにはどうなっていると良いか
    * 異常に早期に気付けるにはどうなっていると良いか
    * 気付いた後の対応を早く行えるにはどうなっていると良いか

### 4. 書いたポストモーテムを Post mortems / Incident review 会で共有、レビュー

書いたポストモーテムを共有し、レビューを行います。

Post mortems / Incident review 会は、 Infra Squad + 各 Domain の SRE チームのメンバーが参加しています。参加するには、[ミーティングの Issue (internal)](https://github.com/wantedly/post-mortems/labels/Post%20mortem%2FIncident%20Review) にコメントをしたり、直接相談などしてみてください。 ポストモーテムを書いたチームが、学びを知ってほしい相手を review 会に呼ぶことも可能です。

レビューでは、主に以下の項目が正しく記載されているかをレビューし、必要に応じてその場で修正を行います。

* 内容が非難を避け、建設的であるか？
* 後々のためにインシデントの主要なデータは収集されているか？
* インパクトの分析は完全か？
* 根本原因は十分に深く分析されているか？
* アクションプランは適切で、その結果として行われたバグの修正には適切な優先順位が与えられているか？
* 結果は関係するステークホルダーたちと共有されたか？
* 長期的な解決策が考えられているか？
* アクションプランや長期的な解決策でトイルが増加しないか？

> 「人を修正することはできませんが、システムやプロセスを修正して、複雑なシステムの設計やメンテナンスを行う際に、人々が正しい選択をすることをうまく支援することはできる」 抜粋:: Betsy Beyer “SRE サイトリライアビリティエンジニアリング”

👉 [駄目な例 (internal)](https://github.com/wantedly/post-mortems/issues/4)

## 書いたポストモーテムの活用

書いたポストモーテムについて、TODO は review 会で assign を決めて取り組んでいます。 Long Term Action は、以後のプロジェクトの検討材料として使ったりなどしています。

(Long Term Action の棚卸しなどはもっと行いやすいように、改善などを進めている最中です 💪 <https://github.com/wantedly/post-mortems/issues/84>)

#### 話を聞きに行きたい

* Slack: [#post\_mortem](https://wantedly.slack.com/archives/C027KFZCU5V), [#infra](https://wantedly.slack.com/archives/C010V922570)

#### もっと知りたい

* [失敗から学ぶ - ポストモーテム / Postmotem culture at Wantedly - Speaker Deck](https://speakerdeck.com/munisystem/postmotem-culture-at-wantedly)


# 負債返済日の取り組み

## TL;DR

* 負債返済日は、月に1回「普段あまり優先度が上がらないが、やったほうがいいこと」をやる日です。
* いわゆる「技術的負債」の返済だけでなく、生産性を上げるためのツールやしくみを作ったり、ドキュメントを書いたりといったこともしています。
* Chapter 単位で行っています。

## Why

一言で言うと、システムや使用技術、開発方法などを、中長期的によりよい状態にしていくためです。

システムや使用技術、開発方法などは、時間を経るに連れて、サービス自体の進化、組織の変化、技術の発展、外部環境の変化などの様々な変化によって、徐々に最適な状態から離れていきます。 これらの最適な状態からの乖離は、溜まっていくとどんどん解消のために必要なコストが増えていくので、こまめに「返済」していくことが大切になります。

一方で、普段各メンバーが所属する Squad ではある程度短期的な成果を目指して活動します。そのような活動をしていると、短期的な成果につながらない活動は優先度が上がらずに、 なかなか取り組めなかったりします。そのため「しくみ」として、月に1回強制的に「普段あまり優先度が上がらないが、やったほうがいいこと」に取り組む日を組織的に設けています。

## What

例として以下のようなことをやっています。が、具体的に何をやらなければいけないというルールはありません。

* 依存ライブラリのアップデート
* 社内ライブラリのメンテナンス
* ドキュメントライティング
* ツールの導入
* CIの高速化
* いらないコードの削除
* リファクタリング
* バグフィックス
* セキュリティフィックス
* flaky テストの解消
* 開発用の便利機能を作る
* ...

### 各 Chapter の過去の取り組み例 (internal)

* [Backend](https://github.com/wantedly/backend-chapter/issues?q=is%3Aissue+%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E8%B2%A0%E5%82%B5%E8%BF%94%E6%B8%88%E6%97%A5+in%3Atitle)
* [Mobile](https://docs.google.com/document/d/1DQnWBX3Xj1AiPTUdtWg1YeRFSp4IOYORdginKK6z9fA/edit?usp=sharing)
* [Frontend](https://github.com/wantedly/dev/issues?q=is%3Aissue+%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E8%B2%A0%E5%82%B5%E8%BF%94%E6%B8%88%E6%97%A5+in%3Atitle+)
* [Infrastructure](https://github.com/wantedly/infrastructure/issues?q=label%3A%22Squad+Goal%22+label%3A%E8%B2%A0%E5%82%B5%E8%BF%94%E6%B8%88%F0%9F%A7%B9)
* [Developer eXperience](https://github.com/wantedly/dx/discussions/categories/dx%E8%B2%A0%E5%82%B5%E8%BF%94%E6%B8%88%E6%97%A5)

## Who

基本的にはエンジニアメンバー全員が参加します。 任意参加にすると、よほど意志の強い人でない限り、Squad でのメインの活動に比べて優先度が低くなり、参加しなくなるという流れになりがちなため、全員参加というルールにしています。 また、Why のところで書いたような背景から「メインのプロジェクトが忙しいから不参加」も基本的には許容しない方針としています。

活動は主に Chapter 単位で行っています。

## When

1ヶ月に1回行っています。

日にちについては、まず Chapter 内では同じ日に行っています。また Chapter 間でも、必須ではありませんが、なるべく合わせるようにしています。 これは、Squad 内に複数の Chapter のメンバーがいるときに、負債返済日に参加中のメンバーとそうでないメンバーとがいると Squad 内コミュニケーションが煩雑になるためです。このような意図のため、Infrastructure のように Squad と Chapter が実質一致しているような Squad は必ずしも日にちを合わせる必要性はありません。

## Where

コミュニケーションを取りやすいようにオフラインで集まってやっていたときもありましたが、今は特に共通のルールは設けていません。 このあたりの運用は各 Chapter に委ねられています。

## How

どのように行うかの運用も各 Chapter に委ねられています。 多くのケースでは、だいたい前日くらいまでにやることを決めておき、当日は朝からそれに取り組み、夕方 Chapter で集まって成果共有をする、という流れで行っています。

やるタスクの決め方は、Chapter 内で相談してアサインしたり、自分自身でやることを表明したりで決めています。


# プロダクトの課題発見及び解決

## はじめに

ウォンテッドリーでは全てのエンジニアやデザイナーがプロダクトを通してユーザーに向き合い、プロダクトに関わる全てのことにオーナーシップを持っています。強いプロダクトビジョンを維持し価値提供を行う源泉はここにあります。ウォンテッドリーにおけるプロダクトマネージメントはこういった組織的機能を指すと考えています。つまりエンジニアやデザイナーがプロダクトマネージメントについて学ぶことは非常に重要なことです。

本章ではプロダクトマネージメントの中でも、PMF後のプロダクトを対象に継続的に成長させる方法ついて紹介します。前半ではプロダクトの課題をどのように見つけるのかについて説明します。後半では具体的な解決アイディアを作って実行していくことや経験を次に活かすためにできることを説明していきます。

## 成長させるためのサイクル

成長させるためのサイクルは次の4ステップから構成されます。次節よりこの4ステップについて順を追って説明していきます。

1. 課題発見/解決の考案
2. 優先度判断
3. 取り込む/実験
4. 結果検証/学習

![Discovering and Solving Service Issues - 成長させるためのサイクル](/files/-MhWAMwZO8uW5MWpfsfR)

## 課題発見

課題発見には大きく定量的アプローチと定性的アプローチがあります。定量的アプローチではユーザー行動に関する数字を見て課題を発見します。一方で定性的アプローチではユーザー視点の感覚で課題を発見します。

また、別軸として探索型と検証型があります。探索型ではさまざまな可能性を見つけるために模索します。一方、検証型ではある仮説を持ってその正しさを確認します。

まずおすすめの方法は**探索型の定性的アプローチ**と**検証型の定量アプローチ**です。定性的なアプローチではアイディアを発散して探索することはそこまで難しくはありませんが、仮説を持って検証するには曖昧で取り扱いづらいことが多いです。定量的アプローチでは仮説を持って分析をすることは比較的容易ですが、発散すると分析のコストの割にあまり情報が得られなかったということが起きがちです。もちろん全てのケースでこの限りではありませんが、留意して取り組むと良いです。ただしAIの活用によって定量的アプローチの探索コストは下がりつつあり、この前提は変わってきています(後述の「AI を活用した定量分析」を参照してください)。

## 定性的アプローチ

定性的アプローチには大きく分けて「本当のユーザーを観察する」方法と「ユーザーになり切る」方法があります。「本当のユーザーを観察する」方法はリスト1.の通りです。ユーザー自身やその課題について習熟していないケースやユーザビリティの課題がないかなどを確認したいケースでは有効です。

リスト1.

* ユーザーインタビュー
* ユーザーテスト

「ユーザーになり切る」方法はリスト2.の通りです。本当のユーザーを観察することに比べ比較的コストが低くすぐに始められます。

リスト2.

* ドッグフーディング
* ストーリーマッピング (カスタマージャーニーマップ)
* CREATEアクションファネル (“行動を変えるデザイン”)
* Hookedモデル (“Hooked ハマるしかけ”)

さて、ここからは上記で紹介した方法についてもう少し詳しく説明していきます。

### ユーザーインタビュー

ユーザーインタビューは、実際にプロダクトを利用しているユーザーに対して実施し、ユーザーの課題やニーズを引き出します。具体的に検証したいことがない場合でなくても、まず大雑把に知りたいことをまとめるが重要です。知りたいことからインタビューを行うユーザーの条件や人数を決めます。次に質問事項を用意する。このとき聞き方や順番を推敲します。最後に、目的や質問事項インタビューガイドとして1ページにまとめます。

また、ユーザーインタビューにおける重要ポイントは質問をオープン・エンドにして自由に答えてもらうこと、自分自身のバイアスを外して誘導を避けることです。他にも重要な心構えがたくさんあります。「ユーザーインタビューをはじめよう - スティーブ・ポーチガル」を参考にしてください。

### ドッグフーディング

ドッグフーディングは、自社のプロダクトを自身で使ってみると言う意味です。まずこの奇妙な名前の起源は諸説ありますが、有名なものとして[「Kal Kan Pet Foodの社長が、株主総会で毎回自社のドッグフードを食べていた」](https://www.computer.org/csdl/magazine/so/2006/03/s3005/13rRUygBwg0)と言う説があります。人間が犬向けの食べ物を食べるという表現からも分かるとおり、開発者自身がフォーカスしているユーザー属性と全く異なる可能性があります。最も開発者は誰よりもプロダクトを知りすぎていると言う点は無視できないです。しかし、それでもドッグフーディングが重要であると言えるのはユーザーとそのプロダクトについてリアリティを持って共感できる方法だからです。特に、ユーザーのストーリーを持って使うことが重要です。それは機能するということ以上に課題を解決したいという意図を持って使うということです。ストーリーを持ってプロダクトを使うことってどういうことだろうと思ったのであれば、次に紹介するストーリーマッピングを合わせて実行してみると良いかもしれません。

### ストーリーマッピング

ストーリーマッピングは、ユーザーの行動と感情をフェーズごとにまとめる手法です。この手法は「カスタマージャーニマップ」と呼ばれていることも多いです。対象がカスタマー(顧客)に限らないため、ウォンテッドリーではストーリーマッピングという呼び名が一般的です。

この手法ではユーザーが見ているプロダクトの状態とユーザーの心境を時系列でグラフィカルに表現します。実践的な手法としてはMiroでスクリーンショットを貼っていき、具体的な状況や心境を書き込んでいきます。ここからギャップ分析を行っていきます。理想と比べて思ったより優れていない箇所がまさに見つけるべき課題です。

SlackやTwitterなど優れたプロダクトの具体例が知りたい場合は「ストーリーマッピングをはじめよう - ドナ・リシャウ」が参考になります。

### CREATEアクションファネル

CREATEアクションファネルは、ユーザーがとりうる行動が実行されるために通過しないと行けない5つのステージで構成されてます。「行動を変えるデザイン ―心理学と行動経済学をプロダクトデザインに活用する - Stephen Wendel」で紹介されています。ユーザーは下の図の順番に思考し離脱する可能性があると考えます。各ステージでどのようなことを考えなければならないかについてまとめます。

**ステージCue**では、プロダクトやその機能を使うことを思い出すためのきっかけがあるかどうかを考えます。思い出してもらうための印象づけ、導線や通知などがあるでしょうか。

**ステージReaction**では、ユーザーが本能的に嫌っていないかを確認します。イライラするような体験がないかを思い出してみましょう。

**ステージEvaluation**では、ユーザーがその機能に十分な価値があるのかを考えます。価値がありそうだと思ってもらえる情報を提示できているのかについて考えてみましょう。

**ステージAbility**では、その機能の利用するための環境が整っているのかを考えます。ユーザーがその行動を容易に実行できないと感じるのであればすぐに離脱します。多くの場合クリック数やタップ数が少ないことが重要です。さらにいうと少ないだろうと**推測できる**ことも重要です。

**ステージTiming**では、今である必要を示す必要があります。常にユーザーはそれをどのくらい先延ばしにしても良いかと考えています。この問題は非常に厄介ですが、今やるべき理由があるのであれば提示するべきです。

![Discovering and Solving Service Issues - CREATEアクションファネル](/files/-MhWAMwaVCTvfFZCxmgw)

### Hookedモデル

Hookedモデルはユーザーがプロダクトの利用を習慣化するためのモデルです。「Hooked ハマるしかけ - Nir Eyal」で紹介されている手法です。繰り返しの利用を期待し、新しく習慣を作る必要があるプロダクトや機能にはうってつけの考え方です。HookedモデルではTrigger, Action, Reward, Investimentの4つのプロセスに分けられます。

![Discovering and Solving Service Issues - Hooked サイクル](/files/-MhWAMwbV7j13AMSTjbz)

Triggerでは、内的・外的な動機でプロダクトを思い出してもらいます。これはCREATEアクションファネルにおけるCueと同じです。ユーザー自身がプロダクトや機能について思い出す心理的な結びつきを作れているか、適切なタイミングで通知を送れているのかを思い出してみましょう。

Actionでは、報酬を期待させ単純な行動をしてもらいます。行動してもらうためにはモチベーションや能力が十分にあるかを確認します。つまりCREATEアクションファネルにおけるEvaluationとAbilityと同じです。

Rewardでは、実際に報酬を与えます。ここからがHookedモデルでは重要なポイントになってきます。社会からの承認、新しい刺激的な情報、スキルの獲得などが報酬に当たります。行動前にどのくらいの報酬かがわからない方がよりハマる可能性が高くなります。あなたのプロダクトにとってユーザーに与える報酬が何かを考えてみましょう。

最後にInvestimentで小さな投資をしてもらいます。投資とは時間や行動をプロダクトに費やすことを指します。投資は将来得られる報酬の期待から行われます。投資をすることで行動一貫性の原則や投資効果により次のTriggerにつながっていきます。このプロセスではユーザーに自分自身の情報を入力してもらえないかと考えてみると良いです。

## 定量的アプローチ

よく使う分析の型として、ステップ分解、セグメント分割、コホート分析を紹介し、あわせて分析の組み立て方と陥りやすい落とし穴をまとめます。最後に、これらの分析を加速させる手段としてAIの活用を扱います。

### ステップ分解

問題となっているユーザー行動が複数のステップに分解できるのであれば分解してみましょう。ここで言うステップとはトラッキング可能なユーザーの最小行動です。多くの場合はクリックやスクロールといったユーザーアクションです。最初に想定した課題で大幅に離脱しているかどうかを確認します。他にも大幅に離脱しているポイントがないか、直感的におかしな箇所がないかを見つけます。こうしたステップと各ステップの通過率をまとめたものはファネルと呼ばれます。ファネルにすると、どのステップで離脱が集中しているかが一目でわかります。どんなステップに分解できるかを考えるときは、前述のCREATEアクションファネルが手がかりになります。

### セグメント分割

問題だと思ったユーザー行動は、特定のユーザーセグメントだけで起きている可能性もあります。もしユーザーセグメント(例えば、ユーザーは学生である、など)が仮定できるのであれば、検証してみる価値があります。ただ、あまりにも小さなセグメントで起きている可能性があるときは注意が必要です。計測結果はノイズが多く偶然起きている可能性が高くなり、改善できたとしても影響が限定されます。

### コホート分析

同じ起点(例えば、登録した週が同じ、初めて特定機能を使った週が同じ、など)を共有するユーザーの集団をコホートと呼びます。コホートごとに時間経過に沿った行動を追うと、単純な合計値では見えない変化が見つかります。特に、施策や外部要因の前後で「新しく入ってきたユーザー」と「以前からいるユーザー」の振る舞いを混ぜずに比較できるのが利点です。時間の経過とともにどれだけのユーザーが利用を続けているか(リテンション)を見るときにも、このコホートの考え方を使います。継続率の裏返しである解約(チャーン)率や、コホートがNヶ月後にどれだけ残っているかという生存率も、同じ枠組みで捉えられます。ただし、コホートを細かく切りすぎるとセグメント分割と同様にノイズが増えるため、意味のある粒度に留めましょう。

### 分析の組み立て方

1つの定量分析は、おおむね次の順序で進めると手戻りが少なく解釈もぶれにくくなります。

1. **指標の定義を固める**: 何を1件と数えるのか、どのデータを正解とするのかを最初に決めます。定義が曖昧なまま数字を出すと、後述の「結果の想定」で触れる数字いじりに陥りやすくなります。ウォンテッドリーではBI上の指標定義を Looker (LookML) に集約しているため、既存の指標の意味は [Looker 入門](https://docs.wantedly.dev/fields/data/looker) で、テーブルがどこから来たデータかは [データ基盤入門](https://docs.wantedly.dev/fields/data/data-infra) で確認できます。
2. **対象を絞り、現状を掴む**: 分析する期間・セグメント・母集団(分母)を決め、まずは分布や代表値で現状を把握します。
3. **セグメント軸で層別する**: 前述のセグメントやコホートの切り口で数字を分け、特定の集団だけで起きている現象かどうかを確かめます。
4. **時系列で変化を見る**: 月次推移や前年同月比(YoY)で、変化とその大きさを確認します。季節性の扱いは後述の「前後比較」も参考になります。
5. **事実と解釈を分けてまとめる**: 「結果(事実)」と「考察(解釈)」を分けて示唆を書きます。後述の「効果検証/学習」の「考察をする」で述べる、結果と考察を分けて書くのと同じ考え方です。

### 分析の落とし穴

定量分析では、集計そのものは正しくても解釈を誤らせる罠がいくつかあります。代表的なものを挙げます。

* **生存者バイアス**: 「今も残っているユーザー/企業」だけを見ると、すでに離脱した対象が抜け落ち、実態より良く見えることがあります。特に解約分析では、離脱した対象を母集団に含められているかを確認します。
* **過去の属性の取り違え**: ユーザーや企業の属性(プランや所属など)は時間とともに変わります。今は有料プランの企業でも、分析対象の当時は無料プランだったかもしれません。過去を振り返るときは、現在の属性ではなく当時の属性で結びつけます。
* **計測単位の不一致**: 同じ「募集の閲覧」でも、イベント数で数えるかユニークユーザー数で数えるかで数字は大きく変わります。特にファネルは、あるステップを表示回数、次のステップをユーザー数で数えると通過率が意味を持ちません。比較する数字の間で単位を揃えます。
* **公平な比較**: 期間を比べるときは経過日数を揃えます(月の途中同士を比べるなら同じ経過日数で比較する)。また、複数の期間やセグメントをまたいで全体のユニークユーザー数を求めるときは、単純合算せず重複を除いて数えます。
* **ロジック変更による不連続点**: 推定モデルや集計ロジックが途中で変わると、その前後で数字が不連続になります。時系列で見るときは変更の前後を分けて解釈します。

### AI を活用した定量分析

定量的アプローチの弱点は「発散させると分析コストの割に情報が得られない」点にありました。AIはこの**探索のコスト**を下げてくれます。つまり「定性/定量 × 探索/検証」のフレームで言うと、AIは定量的アプローチが苦手としてきた探索型の使い方を現実的にしてくれるツールです。ただし、AIの役割はあくまで**分析の加速と仮説の量産**です。どの仮説を検証するか、結果をどう意思決定に使うかという責任は人間に残ります。

**AIが効く場面** 前述の分析の型に対応づけると、AIは次のような場面で効きます。いずれも、指標の定義・対象セグメント・期間・プロダクトの前提といった文脈を明示的に渡すほど出力の質が上がります。また、まず広く出させてから人間が絞り込むこと、出てきた仮説がトラッキング可能な最小行動の粒度まで落ちているかを確認することが重要です。

* 集計・分析の実行補助: 自然言語で依頼するとクエリの作成だけでなく実行・集計まで任せられ、試行回数を増やせます。ステップ分解でファネルを組むときにも向いています。社内ではDevinを使ってこの用途を運用しています。安全に任せるには、AIが参照できるデータを個人情報を含まない集計済みのテーブルに絞っておくことが前提です。
* 異常・離脱ポイントの発見候補出し: 大幅に離脱しているステップや、直感的におかしな箇所の候補を挙げてもらう。ステップ分解で「見つけるべき箇所」の初期リストになります。
* セグメント探索: 人間が思いつかない切り口のセグメントを提案してもらう。セグメント分割の幅が広がります(小さすぎるセグメントに注意する原則はそのままです)。
* 仮説の発散: ある数字に対する解釈の候補を複数出させ、検証すべき仮説リストの初期案にする。

**注意点・落とし穴** AIは探索を速くするぶん、誤った方向にも速く進めてしまいます。前述の「分析の落とし穴」に、AIを使うと踏みやすい罠が加わると考えてください。

* ハルシネーション / 数字の捏造: AIは実在しない数字やそれらしい集計結果を出すことがあります。意思決定に使う数字は必ず一次データで裏取りしてください。
* 前提の見落としを加速する: AIは生存者バイアスや当時の属性(前述)を意識せずにクエリを書くことがあります。出てきた集計が、含めるべき対象や正しい時点を踏まえているかを人間が確認します。
* 相関と因果の取り違え: AIが挙げるのは「相関しそうな仮説」に過ぎません。効果はA/Bテストや対照群付きの比較で検証します。単純な前後比較しかできない場合は、季節性や外部要因などの限界を明記し、因果効果と断定しません(詳しくは後述の「評価方法」)。
* 「数字いじり」(p-hacking)の加速: 切り口を無限に試せるようになるぶん、後から都合の良い有意な指標を探すことに陥りやすくなります。だからこそ、後述の「結果の想定」で述べる、指標と成功/失敗の閾値を事前に決めておくことがより重要になります。
* 再現性: AIとの対話は同じ問いでも出力が揺れます。意思決定に使った分析は、手順や実際に叩いたクエリを残し、後から再現できるようにしてください。

## 解決方法の考案

課題発見で見つけた課題から解決方法を考えて施策としてまとめていきます。施策にはリスト3.の情報を含めます。

* 解決方法 (誰にいつどのように何を提示するか)
* 価値仮説 (想定している、ユーザーの欲求とプロダクトのギャップ)
* 結果の想定
* 評価方法
* 優先度指標

### 解決方法

解決方法はできるだけ具体的に書きます。どんなユーザーを想定しているのか、どのページにどんな条件で何を示すのかなどです。また、任意期間でどれくらいのユーザー数に表示されるかを把握すると良いです。このユーザー数は、この後の優先度判断や評価方法で利用します。さらに、もしUIが自明でない場合、ワイヤーフレームなどを利用しプロトタイピングすると良いです。多くの場合、UIを言葉だけで100%理解してもらうのは思ったより難しいので、プロトタイピングは常にお勧めです。

### 価値仮説

ユーザーに対してどんな価値があると考えているのかを説明します。課題発見で発見した「課題」から「価値」に接続させ「解決方法」の正当性を支持します。具体的には、ユーザーの欲求とプロダクトのギャップを説明します。

### 結果の想定

解決方法が**本当の意味で正しかった**と言えるには、成功シグナルを見つける必要があります。良い成功シグナルには次の条件があります。

* 実際のユーザーの行動指標である
* 成功/失敗するとき、必ず影響する指標である
* 計測しやすい指標である
* 短い期間で反映されやすい指標である

指標が見つかったら成功と失敗の閾値を決めましょう。具体的にどれくらいに設定するべきかは一概には言えないですが、プロダクトの主な指標から逆算して「どれくらいなら気に掛けるか」と言う点は重要です。指標の変化に対する感度はチーム全体で共有されていると良いです。 最後に、重要なのが**この想定が事前に**必要な理由です。このプロセスが回避するのは次の惨事です。

* 実は計測できない指標であった
* チーム内で結果の賛否が別れて、数字いじりに陥る

### 評価方法

評価方法とは、「結果の想定」で示した指標の変化をどのように計測するかです。比較的簡単に取れるのは、A/Bテストと前後比較です。最初にA/Bテストを検討し、難しい場合に前後比較を検討します。なぜなら、A/Bテストは解決方法の影響のみを評価できますが、前後比較は解決方法以外の影響も受けるため非常に厄介だからです。

### A/Bテスト

A/Bテストを含むランダム化比較試験では、ユーザーをランダムに選び実験群と対照群に分けます。実験群に対して解決方法を取り込み、対照群には取り込みません。その結果、計測指標に対して各群の差を比較すれば解決方法の影響を示せます。

さらに、各群の差が偶然でないことを示すためには、十分なサンプル数が必要です。必要最低限のサンプル数はベースライン、最小検出可能効果、信頼水準から計算します。Optimazlyが提供するCalculatorを使うことで簡単に計算できます。「必要最低限の合計サンプル数」を「任意期間あたりのアクセスユーザー数」で割ると必要な実験期間がわかります。実験期間は2週間以内が望ましいです。2週間より多くかかりそうな場合、A/Bテストを断念するか実行後に期間を区切って終了することをお勧めします。A/Bテストを長期化する場合は改悪するリスクが増えることを念頭に置く必要があります。

* [A/B Test Sample Size Calculator](https://www.optimizely.com/sample-size-calculator/)
* 『A/Bテスト実践ガイド 真のデータドリブンへ至る信用できる実験とは』 - Ron Kohavi他

### 前後比較

前後比較では、解決方法を取り込む前の期間と後の期間での指標を比較します。非常にシンプルに見えますが、解決方法以外の因子があるので厄介です。前後比較をしたときに、結果指標に十分な差があり、解決方法以外の因子を全て検討した上でどう考えても解決方法の影響だったと言える必要があります。ここで最も気をつけるべき因子は、**季節性の変化**や**同時期に実施された他の変更**です。多くの場合はこの簡易方式で十分ですが、もう少し形式的には多変量解析があります。詳しく知りたい場合は「効果検証入門～正しい比較のための因果推論/計量経済学の基礎 - 安井 翔太」が参考になります。

## 優先度判断

優先度判断のためのフレームワークは様々なものがありますが、ここでは社内で最もよく使われるICEスコアについて解説します。ICEスコアは多角的に評価できシンプルであると言う意味で優れています。他のフレームワークも知りたいのであれば[プロダクトマネジメントの優先順位付けフレームワークの究極ガイド](https://zenn.dev/pm_translate/articles/054e6e384062f4#優先順位付けフレームワーク)を参考にしてください。

ICEスコアの定義は下の式で表すことができます。各項目は1-10の10段階で評価します。各項目の定義は下の通りです。具体的な基準はチーム内で話して作ると良いです。

![Discovering and Solving Service Issues - ICEスコアの定義](/files/-MhWAMwc4bucQhUlWoKD)

* Impact: 主要な指標に対してどれくらい影響するか
* Confidence: どのくらいの確率で成功すると考えているか
* Ease: どれくらい簡単に実装可能か

優先度を判断するには誰もが見えるところにリストを作成します。どのツールを使っても良いですが、ICEスコアで簡単にソートできることが重要です。内容としては、タイトルとICEスコアと詳細リンクがあれば最低限は問題ないです。ICEスコアを高い順番に並べ違和感がないかをチームで議論します。ICEスコアを調整して妥当だと感じたのであれば、上から順に取り組みます。

## 効果検証/学習

### 結果をまとめる

A/Bテストの場合、各群の指標の値とp値を示します。Rで母比率の比較をする場合`prop.test`を用います。次の例では対照群でのCVRが`120/7000`で、実験群のCVRが`161/7000`の場合の結果です。p値が`0.01593`で有意水準`0.05`を下回っているので有意な変化だと言えます。

```r
> prop.test(c(120, 161), c(7000, 7000))

	2-sample test for equality of proportions with continuity correction

data:  c(120, 161) out of c(7000, 7000)
X-squared = 5.8106, df = 1, p-value = 0.01593
alternative hypothesis: two.sided
95 percent confidence interval:
 -0.010645214 -0.001069072
sample estimates:
    prop 1     prop 2
0.01714286 0.02300000
```

この例だと次のようにまとめられます。

```
## 結果
p値が0.015のため、CVRの32.5%増加は有意だと言える。

- 対照群CVR: 1.7%
- 実験群CVR: 2.3%
```

前後比較の場合、前期間と後期間それぞれの指標の値と指標に影響しうる他の変更や要因がないかをまとめます。例えば次のような文章です。

```
## 結果
変更前と比べて変更後ではCVRが32.5%増加している。昨年同時期の変化率は5.4%であり季節的な変化の影響より今回の変更の影響が支配的だと考える。また、同期間に同一ページでの変更は行われていない。

- 前の2週間CVR: 1.7%
- 後の2週間CVR: 2.3%
```

### 考察をする

議論の余地がある点を明確にする必要があるため、結果と考察を分けることが重要です。結果ではなるべく事実のみを述べ、考察では発見したことや自身の考えを述べます。想定した結果に対して実際はどうだったのか、つまり成功したのか失敗したのかを言及します。成功した場合と失敗した場合では次のようなことを軸にして考えをまとめると良いでしょう。

* **成功した場合**
  * ユーザーが本当に意図通りの行動をしているか他のシグナルを調べる(追加証明)
  * 追加でわかった事実はないかを探す
  * 別の問題が発生していないかを探す
* **失敗した場合**
  * 意図通りにユーザーが行動しない可能性をリストアップする
  * 実際に行動が阻害した結果がないかを調べる
  * 次の仮説を作る

### 結論

最後に結論をまとめます。後から見返したとき最も重要なパートです。結論では成功の可否、結果と考察の要約が3行程度で記載されていると良いでしょう。

## まとめ

プロダクトの改善プロセスを「課題発見/解決の考案」「優先度判断」「取り込む/実験」「結果検証/学習」というフェーズに分解し活用できるテクニックを紹介してきました。課題発見では定性的アプローチと定量的アプローチがあります。定性分析ではドッグフーディング、ストーリーマッピング、CREATEアクションファネル、Hookedモデルなどさまざまなモデルがあり、機会があれば試してみると新しい発見につながります。定量分析ではステップ分解、セグメント分割、コホート分析といった型を使います。AIは探索を速くしますが、検証と意思決定は人間が担うことが重要です。解決方法の考案では、解決方法、価値仮説、結果の想定、評価方法、優先度指標の項目に分解してまとめていきます。優先度判断では、ICEスコアをつかってチームで効果的だと思える施策から取り組めるようにします。実験をした後の効果検証/学習では結果と考察を分けて述べます。考察では失敗しても成功しても学びを深めます。このように施策を回していけば、必ず次の施策につながります。サイクルを回せば回すほどチームの集合知が高まっていき効率良く改善できているのを感じるでしょう。この漸進的な活動こそが改善フェーズにおけるプロダクトマネージメントです。全てのエンジニアがプロダクトマネージメントに興味を持ってもらい、ぜひどんどんチャレンジしてもらえたら嬉しいです。

#### 話を聞きに行きたい

* Slack: [#product\_management](https://wantedly.slack.com/archives/C01SYNKAC3W), [#products\_products](https://wantedly.slack.com/archives/C028QUT3NQ7)


# ソフトウェアデザインの基礎

この文章は、ウォンテッドリーで行ったソフトウェア設計についての新人研修『ソフトウェア設計の Why & What & How』で話した内容をまとめたもので、全員に知っておいて欲しいソフトウェア設計の基礎概念について取り扱っています。

ソフトウェアの設計というのは関連する領域が広いため、どうしても断片的な理解になりがちです。そこで、早い段階で全体像を感じてもらうために、ソフトウェア設計の Why と What と How をつなげて話しています。

## この研修のねらい

### はじめに

ソフトウェアの設計について書かれた情報は世の中に多いですが、その情報の多くは How であり、それだけを読んで適切に使うことが難しいと感じています。その直接的な理由は、How に対しての What、How / What に対しての Why が語られることが少ないからです。

ただ、How だけを知っていると、それは本当に問題を解決しているのかを考えることができず、場合によってはどこにも行きつかないということにもなります。How に閉じずに、目的志向で考えることは、相応の思考を要求されることではありますが、やはり必要なのです。

そこで、この研修では、Why から始めたいと思います。

### スコープ

とはいえ、1時間の研修で話せることには限りがあります。

そこで、この研修の中では、重要だが意外と見落とされがちなことにフォーカスします。特に、Why と What と How をつなげること、及びその中で、世の中的によく出てくるキーワードを使用すること、それらのキーワードが実際にはどのように接続しているかということ、つまり各種の関連を重視します。

そのようなキーワードを持ち帰って、今後のプログラミングの中で、自分で考える材料にしてほしいと思います。

![](/files/-MhWAN-1VOmtRi_Hc2xG)

## Why

### なぜソフトウェアに設計が必要になるか

ではソフトウェアの設計はなぜ必要で、なんのために行うのでしょうか。ここでは、二つの異なる見方をまず紹介します。

* 保守的な見方：ソフトウェアの設計は**複雑性**をコントロールしながら開発し続けるための必要経費である
* 進歩的な見方：ソフトウェアを設計することはソフトウェアが何であるかを**定義づけること**そのものである

ここでの保守的と言う言葉は、そのことで困らない、それがボトルネックにならなければ良いというイメージです。複雑になることで開発が継続できなくなる、ということにならないように設計していく、っていうことですね。

逆に進歩的という言葉は、そのことが新しい認識を作ったり、新しい価値を作るというイメージで使っています。流行りの言葉で言えばいわゆる「デザイン思考的」な考え方に近いと思います。ここでは、「設計」は英語で “design” であるという事実を思い出せば十分でしょう。

いきなり二つの見方がある、という話をしてしまいましたが、あえて二つ紹介したのはどちらの見方も有用だからです。

たとえば僕自身は、普段のアプローチとしては、ほとんどの小さな問題には保守的に取り組みます。ただ、たまに出くわす大きな問題には両方の視点から考えます。これは、ソフトウェア・デザインのレベルで大きなアイデアを持ち込まないと解決できない問題がしばしばあると感じるためです。

今回の講義では、頻繁に出くわす問題にうまく対処できることが第一歩であるという考えで、複雑性をベースに話を進めていきます。

ただ、実際には、困らないためにやった設計が、しばしば新しい価値を産むデザインであったりもするので、ここではこのふたつを対立的なものとして扱う必要はないですし、むしろ同じものの別の側面だと思って欲しいと思います。覚えておいて欲しいのは、定義の問題が顔を出す場合には定義それ自体に向き合う必要がある、ということです。

![](/files/-MhWAN-2XQWOqlg-VZeq)

### 「複雑性」（Complexity）

それでは複雑性に話を移しましょう。とっかかりとして、1986年にフレデリック・ブルックスという人が書いた論文の一節を引用してみます。

*“すべてのソフトウェア構築には、本質的作業として抽象的なソフトウェア実体を構成する複雑な概念構造体を作り上げること、および、偶有的作業としてそうした抽象的実在をプログラミング言語で表現し、それをメモリスペースとスピードの制約内で機械言語に写像することが含まれている”*

このブルックスという人は IBM で OS/360 という OS を作ったりしていた人です。OS というのは高度なソフトウェア・システムの代表例ですから、この文章は複雑性の高いソフトウェアを開発したら何がボトルネックになるのか、という視点で読むと良いと考えます。

ちょっと読み上げてみますね。・・。

難しい言葉が多いですね。ちょっと一個一個みていきましょう。「複雑な概念構造体」なるものをコードで表現して、実行することが全てのソフトウェアを構築するという作業には含まれてる、ということらしいです。次のような質問をしてみましょう。

「複雑性が高いってどういうこと？新しい概念ってどんなもの？」 「複雑性が高いままだとどう困るの？」 「ソフトウェア以外の構築物とソフトウェアってなんか違うの？」

順を追って見ていきます。

### 複雑性につながる新しい概念の例

「複雑性が高いってどういうこと？新しい概念ってどんなもの？」

Web アプリで実際にものをみていきたいところですが、実は、単純なコンテンツ・サービスであれば Web の前提に乗れるので、Web アプリケーションのフレームワークを使っていればプログラムはそこまで複雑にはなりません。しかし、思わぬところで新しい概念が登場することがあります。

例えば、ミニマルなものとしてはこれです。

![](/files/-MhWAN-3sMUV6Dv1P1Sq)

これ（左）は Wantedly の募集に載るユーザー情報です。ところで、これは何でしょうか？プロフィール（右）にも同じようなラベルがあるように見えるのですが、それと同じなのでしょうか？

ひとまず、その人の所属・役職を一言で表したものな気もします。だとしたらどのように計算されるのが良いでしょうか。職歴のリストから計算されるのが妥当でしょうか。学生の場合はどうでしょうか。学歴は計算対象でしょうか（ビジネスSNSにおいてユーザーを名前とラベルで表現するとしたら、それは何であるべきか、という定義の問題に行きつきそうです）。

そして、おそらく、募集ページに載る情報はそれと同じではないかもしれません。共有している部分はありますが、募集を出している会社における役職、という方が正しそうです。

こういった質問をせずに、ロジックを適切に共有したり関数として分離したりすることをしないまま開発を続けていると、後述するようにいろいろと問題が起きます。実は、ここで出した例はこのように定義できていなかったために、退職した後に別会社の役職になってしまう、という問題が長らくありました。

Web のコンテンツサービスを作る場合、Web や Rails がレールを敷いてくれています。しかし、自分たちのサービスに固有の複雑性は自分たちで対処する必要があります。実際、Rails の作者である DHH も次のように言っています。

*“しかし、何事もそうですが、慣習の力には危険がないわけではありません。Railsがこれだけ多くのことをあまりにも取るに足らないようにみせていると、アプリケーションのあらゆる側面は既成のテンプレートで作れるのではないかと考えがちです。しかし、構築する価値のあるほとんどのアプリケーションには、何らかの方法でユニークな要素があります。それは5%や1%に過ぎないかもしれませんが、そこに存在しています。”* - The Rails Doctrine - DHH(訳：高橋征義)

これは個人的な意見ですが、DX やら IoT やらでソフトウェアのフロンティアが拡張され続けている現在、実世界とつながるサービスが増えてきて、過去よりも単純なウェブ上のコンテンツサービスでは済まなくなってきている傾向があるように感じています。そのため、ソフトウェアの設計が活きる部分というのもまた増えていく、つまり設計が重要性を増しているのではないか、と考えています。

身近なところで言えば、この「5%や1%」の割合はウォンテッドリーにおいては上がっていると感じます。たとえば、従業員のモチベーションを管理するサービスを作るのであれば従業員のマネジメントとはどのようなものとして定義するのが妥当か、というようなことを考える必要が生まれますよね。これはコンテンツサービスよりも複雑性があります。

ここで挙げた話はバックエンドのいわゆるドメイン設計と呼ばれるものですが、フロントエンドで言えばリッチな GUI アプリケーションの複雑性は高くなりがちなので、似たような話は別の領域でもあると思っています。たとえばプロフィール機能を作っていても、それなりに設計をしないと開発が難しくなる複雑なアプリケーションだなと思います。

### 複雑性に対処しないことで表出する問題

「複雑性が高いままだとどう困るの？」

少し抽象的な話が続きましたが、複雑性によって表出する問題の典型は次のようなものです。

* Change amplification：変更箇所がめっちゃ増える、くらいの意味
* Cognitive load：直訳すると「認知負荷」。要するに「コードが読めん、わからん」という状態。
* Unknown unknowns：あるコードを修正する際に、どの条件を満たしたらそのコードを修正しきったかが把握できない。

これは身に覚えがある人がほとんどなのではないでしょうか。

保守的に言えば、これらが開発のボトルネックにならないように対処していくことが大事です。「ならないように」と言っているのは、多くの場合は「なってから」対処するよりもならないようにする方が安いからです。特に Wantedly Visit のような継続性が見えているサービスの場合やよりそうです。このあたりの塩梅や考え方は、How のパートでも話します。

ちなみに、この問題の分類の出典は “A Philosophy of Software Design” という本です。2018年に出た本ですが、複雑性という概念を軸にソフトウェア設計の勘所についてまとめた良書なので、英語ですが興味のある人は読んでみることをお勧めします。

![](/files/-MhWAN-4NJcWbzDYiSX4)

### 建築物との対比

「ソフトウェア以外の構築物とソフトウェアって違うの？」

ソフトウェアの特性を理解する上で、ソフトウェアでないものと比べることは有用だと考えます。ここでは建築物と比較してましょう。単純に考えて、たとえば100階建てのビルという建築物で、25階と99階に特有の依存関係、例えば階段があることはないですよね。あとは建て増しとかするときにも、空中で横方向に無限に建て増しが行われるとかないです。

余談ですが、「制約」というキーワードは複雑性を考える上で実はとても重要です。制約があると、複雑性が減ります。Web というシステムもリソースは一意な識別子で表現され GET や POST など5つのメソッドでしか変更できない、という制約によって複雑性が減り、発展していきました。

## What

複雑性に対処しなければならないことはわかったけど、じゃあそれってどういうことなの？というのがこのパートのお話です。つまり複雑性に対処する行為を「設計」と呼ぶことにしたとして、それはどういった行為でしょうか。これもまた一度立ち戻って考えましょう。

そのためには、ソフトウェア以外のところからヒントを得ることにします。

人間の営みを考えてみると、この現実の世界は複雑なわけですが、それを有限の思考能力でそれなりに理解して、複雑な道具を使いこなしたりして生きているわけですよね。

ソフトウェアも（少なくとも当面の間は）人間が読み書きするものなわけですから、同じような形で複雑性に対処することになります。

ここでは、「時計」と「アプリ」と「スタック」を例にして、いくつかの異なるキーワードを使ってそのことを見ていきます。

![](/files/-MhWAN-5hquQuHc9DdXX)

![](/files/-MhWAN-6AWI6GhZ8NG0j)

### 「抽象」（Abstraction）

よく、設計について語る際、「抽象化」という言葉が出てきます。では、その結果生まれる「抽象」ってなんなんでしょうか。なんで、抽象化をすると、設計ができたことになり、ソフトウェアが開発しやすくなったりするのでしょうか。

実は、「時計」「アプリ」「スタック」はどれも抽象です。はい、よくわかりませんね。しかし、今僕たちが「時計」だと思っているものがやっぱり抽象なのです。ひとまずのところ、これらのもので僕たちが便利に生活できていることは、まあ直感的に疑問を挟む余地はないのではないでしょうか。

抽象って誰が生み出すのでしょうか。「時計」は人類ですね。「アプリ」は、考えた人は知らないけど、広めたのは Apple っていう会社でしょう。「スタック」はコンピュータ・サイエンティストです、きっと。

![](/files/-MhWAN-7B2lLQ5F1dj2g)

補足：[Abstraction](https://en.wikipedia.org/wiki/Abstraction) は概念化する過程とその成果物の両方の意味があり、冠詞で区別できる。ここでは区別のために "Abstraction" に「抽象化」、"An abstraction" に「抽象」という語を当てている。

### 抽象いろいろ

いろいろな領域（ドメイン）に対して、色々な抽象があります。GUI オペレーティングシステムの領域には「アプリ」以外にもさまざまな抽象が含まれていますし、ソフトウェア一般の領域には「スタック」以外のデータ構造やデータのやり取りの仕方、などの抽象があります。

サービスを作る際に、そういう「スタック」のようなすでにある抽象は便利に使えば良いわけですが、僕たちサービスの開発者が設計・デザインすべきものは何だと思いますか？

アプリケーションのプログラマーであれば、アプリケーションそれ自体の領域（ドメイン）に含まれるものをデザインすべき、と言えるかもしれません。これは「ドメイン駆動設計」と言う考え方で知られています。

まあこれ自体は考え方の派閥の一つなのであまりそのことにはこだわらないでおきましょう。フロントエンドのプログラマーであれば、UI に特有の振る舞いを抽象化する、ということも必要でしょうから、そういうことも含めて言っています。あ、この意味で、「UIデザイナーの仕事に興味を持つ」みたいなのはめっちゃ重要です。対象に興味を持つのは良い設計をするための必要条件です。

あとうちの dx チーム（注：Developer Experience = 開発者の体験を最大化することをミッションとしたチーム）が作っている “kube fork” なども抽象化の良い例ですね。

（ちなみに、「抽象」と似たような言葉に「モデル」と言う言葉があります）

### 「インターフェース」（Interface）

抽象化を行う際、「インターフェース」という言葉もよく出てきます。インターフェースについて説明するためには、一つ前提となる考えに戻らなければいけません。

それは、仕様は実装より容易に理解できる、という見立てです。

抽象化を施したソフトウェア部品を「コンポーネント」と仮にここでは呼ぶことにしましょう。コンポーネントには仕様と実装があります。つまり、外から見て期待する振る舞いと、それを内でどのように実現するかという二つの観点です。そして、うまく抽象化されている場合、実装がコンポーネントの内側に隠蔽されるため、仕様は実装よりもずっと理解がしやすいものになるはずだ、という見込みがあります。この考えの元では、仕様と実装をきちんと区別することが重要になります。

仕様と実装を区別しようと思うと、ほとんど必然的に仕様と実装の間にある「境界」に着目することになります。それを「インターフェース」と呼びます。

> “*Interface: a point where two systems, subjects, organizations, etc. meet and interact.*” - 英英辞典

![](/files/-MhWAN-8ZM3BHXN76RoU)

### インターフェースいろいろ

例に戻りましょう。「時計」は時刻が円状に分割され長針と短針で時刻を読み取れるようなインターフェースを持っています。このインターフェースがあることで、時計がどのように動作しているのかを理解することなく時計から時刻を読み取ることができます。しかも、色々な時計があっても、それを共通で時刻を理解するものとして理解し、利用することができます。すごいですね。

では「アプリ」のインターフェースってなんだと思いますか？では、当てますね。・・・。

正解はありませんが、僕は「アプリ」は、アプリストアから入手できて、ホーム画面に並べられる視覚的な記号と結びついていて、タップしたら使い始められるインターフェースを持っているもの、と考えました。これによって、たくさんの人がスマートフォン・デバイス上でアプリケーションを使うことができています。ユーザー・インターフェースは、要するにこのようなものです。

では「スタック」のインターフェースってなんでしょう？これはもしかしたらわかりやすいかもしれません。当てますね・・・。

そうですね。〈なにか〉をプッシュでき、ポップすることでプッシュした順番とは逆順に〈なにか〉が出てくるインターフェースを持っています。この定義も完全ではありませんが、全てを言わずに要点だけをいうことにインターフェースの利便性があります（余談ですが、この〈なにか〉は多くの型システムにおいて「ジェネリクス」ないし「型パラメータ」として表現されますね）。

![](/files/-MhWAN-90tcCDTU9poOZ)

プログラミング・システムにおいて、抽象を定義する手段であるインターフェースは非常に重要なものなので、発展がみられます。

ちょっと昔のプログラミングの本を読むと、関数のシグネチャについてちょっと重視している雰囲気が伺えます。それからオブジェクト（クラス）のインターフェースを定義するメカニズムが導入され、今では API のスキーマを定義するインターフェース定義言語である Protocol Buffers を僕らは使っています。

そのくらい、インターフェースは重要なものです。

### （休憩タイム）

ここから少し次の話に進むので、ちょっと休憩しましょう。少し質問、コメントを見てみます。

> 考えてみたら一個一個の具体的なものじゃなくて「インターフェース」という言葉があるの、不思議な気がする。

これはそうですよね。僕も資料を作っていて、インターフェースという言葉があるの偉いなと思いました。「インターネット」という言葉があるように、"inter" というプレフィックスが存在するので、英語にはもともとそういう概念が存在するような気がします。日本語でそういう概念があまり思いつかなかったので、知っている人がいたら教えて欲しいです。

### 抽象を組み合わせること

ここまででも結構いろいろなことを話してきたのですが、実際のシステムと繋げて考えるために、もう少し続けます。

これまで話してきた抽象やインターフェースというのは主として個々のコンポーネントの説明でした。実際には、こういったコンポーネントを組み合わせることでより複雑なシステムを作ることになります。

コンポーネントは、単に組み合わせてもいいし、**層**（**レイヤー**）のようなコンポーネント依存先を制限する構造的制約（**アーキテクチャ**）を設けることもあります。

ちなみに、依存というのひとつのキーワードで、これが多ければ多いほど、同時に理解しなければいけないものの数が増えます。ただし、依存しているものがよく抽象化されていれば、一個一個の理解が容易になるという関係はあります。

コンポーネントを分けたりするなどして複数のコンポーネントが存在する場合、問題が起きたりします。たとえば、

「このロジック（ないしデータ）はどのコンポーネントに存在するんだろう？」 「どのコンポーネントを変更すれば良いんだろう？Aを変更しても変更できるし、Bを変更することでも実現できるんだけど」

という経験は誰しもあると多います。How に近い、テクニカルな話になりますが、これに関連してよく出てくるキーワードがあるため、紹介していきます。

### 「責務」（Responsibility）

単純な話、登場人物が複数いると誰がどこまでやるかという役割分担が必要になるわけですね。その役割分担に使われる概念が責務です。

責務は、だいたいの場合、そのコンポーネントが行うべき仕事の範囲を文で定義します。外からの期待について述べているという点で、ちょっとインターフェースに似ていますよね。これらは、厳密な関係を追求するよりは、物事の見方や記述の仕方の違いだと考えておくと良いかなと思います。

責務は説明的である分、ファジーですが、いろいろなところで使えるという利点はあります。例えば、特定のコンポーネントではなく、コンポーネント群が含まれる「レイヤーの責務」というものを考えることができます。

たとえば MVC とかで「これはコントローラーの責務じゃないですね」というような会話ができます。これは、コントローラーというレイヤーに含まれる個々のコンポーネントに一般的に期待される責務から逸れている、ということを言っているわけですね。

![](/files/-MhWAN-AQCMAGEXzBwvF)

### 「関心」（Concern）

似たような言葉として、「関心」というキーワードもときどき出てくるので、一応紹介しておきます。

これはソフトウェアの変更が行われる観点のようなものです。例えば、React の入門のページ(react.org)を見ると、JSX の導入のところで次のような説明が出てきます。

“*マークアップとロジックを別々のファイルに書いて人為的に｢技術｣を分離するのではなく、React はマークアップとロジックを両方含む疎結合の「コンポーネント」という単位を用いて｢関心｣を分離します。*”

これは JSX というものが見た目が少し突飛なので必要性を説明するために書かれているのだと思いますが、関心に基づいて抽象化を行って成功した例です。期せずして、ここでもコンポーネントという言葉が用いられていますね。

ポイントとしては、分割の仕方について考えることは抽象について考えることと同じくらい大事だということです。表裏一体と言っていいと思います。

## How

最後のパートでは How について。ここでは自力を上げるための「備え」、プログラムを書くときの「スタンス」、良い設計を実現してくための「プロセス、の三つお話しします。

### 備え：概念を知り、技法を知る

良い設計をするための自力を上げる方法を、二つのアプローチで紹介します（どちらが良い、ということはありません）。

* 教科書的アプローチ：関数型プログラミング、オブジェクト指向プログラミング、マルチパラダイム、・・
* 獣道的アプローチ：異なるパラダイムの言語・フレームワークに触れる。良いコードを読む。

一つ目は教科書的アプローチの方で、これはプログラミング自体について扱った本を読むことです。特定のパラダイムに特化した本もありますし、それらを並べて扱った本もいくつかあります。もちろん、ここで挙げているものはソフトウェア一般の領域のことなので、開発しているプロダクトや事業のことはそれぞれ理解する必要があります。

二つ目は獣道的アプローチで、実際に触れてみる、ということですね。これは日々の業務の中でもできることだと思います。

ここで一点だけ、学習をする上での注意喚起。世の中にないろいろな設計論・アーキテクチャ論がありますが全て目安であって、ルールとして捉えるべきではないです。ルールをそのまま適用すれば良い設計のソフトウェアができる、というようなことはありえないです。

というか、ここまでの話の中で、アーキテクチャなどというものは単なる構造的制約であって設計のごくごく一部であるということは伝わっているのではないでしょうか。知識を摂取するとき、この設計論はどういう目的で何を解決するのか？というような目的を考える視点を常に持つようにしましょう。

![](/files/-MhWAN-BAQc09tTO7qqP)

### スタンス：完璧な設計ができれば良いのか？

さて、この研修は設計についての研修でしたが、それを実行する上でどのようなスタンスでいれば良いでしょうか。

ここでは、「完璧な設計ができれば良いのか？」という問いを敢えて立てました。良いエンジニアリング v.s. 良いプロダクト、とも言い換えてもいいかもしれません。これについて、僕の尊敬するプログラマーの中島聡さんの言葉を紹介します。

*“あのね、私が昔から言っていることがあるんですけど、コードの1行1行というのは経営判断なんですよ。例えば、「このプログラムを使ったら完璧に近いんだけれども、どうしても時間がかかってしまう」という選択肢と、「このプログラミングだったら手っ取り早く動かすことは可能だけれど、いろいろ問題点もある」という選択肢の二択を迫られる場面というのが、エンジニアには常にある。*

*結局、どっちのコードを使うかで、サービスの質が変わったり、事業の収益に影響が出たりするわけだから、プログラマーの営みというのは、いつも経営判断の繰り返しなんです。科学者とエンジニアの最大の違いがここにあると言ってもいい。その自覚を持ってやっているエンジニアになるのかならないのか。それで大きく違ってくる。”*

経営判断、っていうとちょっと大仰に聞こえるかもしれませんが、「このプログラムを使ったら完璧に近いんだけれども、どうしても時間がかかってしまう」という選択肢と、「このプログラミングだったら手っ取り早く動かすことは可能だけれど、いろいろ問題点もある」という選択肢がある、というのはわかると思います。

エンジニアは真実を追求する科学者ではないので、この二つを常に天秤にかけながらプログラミングをしていくことが必要で、対立軸ではないということですね。

### じゃあどうすればいいの？

とはいえ、じゃあどうすれば良いの？という話はあると思います。

「今すぐ機能をリリースしたい」「まだアプリケーションのコアドメインが固まりきっていない」

あると思います。

この解法としては、問題を静的なプログラムそのものではなく、時間軸を伴うプロセスとして捉えましょう。ある時点で完璧なことを目指すのではなく、やっていく中で徐々に良いものに近づけていく、ということです。良いデザインは良いプロセスから生まれる、と弊社のCDOも言っています。

### リファクタリング

だんだんとソフトウェアの設計を改善していく行為がリファクタリングです。

そしてそのリファクタリングを行いやすくするのが、テストです。テストも、ただ書けば良いというわけではなく、リファクタリングを支えるように書かなければ意味がないので、そう単純ではないですが・・。

リファクタリングを行う中で、語彙が足りなければ「言葉」（ユビキタス言語）を定義することにもなるでしょう。そして、いまいちだったら書き直せば良いです。

設計・リファクタリングを楽にやるコツを三つ紹介します。

一つは、ちょっとしたことから始めるということです。冒頭、すごくよくできた抽象を紹介しましたが、関数の名前や引数などといった、小さなところから始めれば良いです。

二点目は、あとから変えられるものと変えにくいものを区別することです。極端な話、Java などのプログラミング言語の標準ライブラリの API は、一回決めたらほとんど変えられないです。それと比べれば、僕らの運営するサービスで後から変えられるものは多いです。ただ、モノリスの関数と違ってマイクロサービスのエンドポイントはずっと変えにくいので、そこは設計に少しコストをかけた方が良いでしょう。

三点目は、全てに言えることではありますが、重要なものにフォーカスするということです。例えば誰も読みに行かないし、事業上もそこまで重要ではないコードをリファクタリングすることの価値は低いです。

全体的に、この辺りのコツは「リーンスタートアップ」にも通じるものがあります。弊社の推奨書籍の一つですね。

お話は以上となります。この研修を作るにあたって直接引用したもの、影響を受けたアイデアを含む書籍・記事を挙げておくので、もっと知りたい人は読んでみると良いかもしれません。

![](/files/-MhWAN-CAfcdQ1wQRIR_)

#### もっと知りたい

* [Google Engineering Practices Documentation](https://fujiharuka.github.io/google-eng-practices-ja/)
* [ソフトウェア設計の Why & What & How](https://speakerdeck.com/altech/software-design-illustrated-by-why-and-what-and-how)


# コーディング規約

## はじめに

コーディング規則はコードベースの品質保持と一貫性を保つ上で欠かせない要素です。世の中では多くの企業が自身のコーディング規約を定め運用しています。 しかしこれらの規約を文章として明文化するだけでは強制力がなく時間と共に形骸化してしまいます。 そこで私たちはコーディング規約を Linter の設定ファイルとして管理する方法を採用しています。

## ウォンテッドリーのコーディング規約の考え方

ウォンテッドリーではコーディング規約を以下のように考えています。

* みんなが読みやすいコードにするためのツール。
* 人間が頑張らない。Formatter に任せる。
* 暗黙の了解にしない。Linter でコード化する。

ウォンテッドリーにおいてコーディング規約は絶対に守らなければいけない規則ではなく、あくまでみんなが読みやすいコードを目指すための道具です。その点コーディング規約を各リポジトリに導入するかどうかは自主性に委ねられています。 また定めたコーディング規約を人間が完璧に守ることはしません。現実的には new joiner が全てのルールを把握することは不可能に近く、慣れた人であってもうっかり規約から外れてしまうことはよくあります。現代においてこのように課題に対しては Linter と Formatter が有効なのでこれらのツールを使います。

## 各言語の Linter 設定

プロジェクトで使用する各言語の Linter 設定ファイルは以下の場所にあります。 これらの設定は言語によって一部カスタマイズされています。

* Backend
  * Ruby: [wantedlycop (internal)](https://github.com/wantedly/wantedlycop)
  * Golang: [golangci/golangci-lint](https://github.com/golangci/golangci-lint) の config ファイル (リポジトリごとに配置)
* Frontend
  * TypeScript: [frolint (internal)](https://github.com/wantedly/frolint)
  * Swift: [SwiftLint](https://github.com/realm/SwiftLint) の `.swiftlint.yml` ファイル (リポジトリごとに配置)
  * Kotlin: [Detekt](https://github.com/detekt/detekt) の `detekt.yml` ファイル (リポジトリごとに配置)
* その他
  * Protocol Buffers: [apis (internal)](https://github.com/wantedly/apis/blob/master/.protolint.yaml)

## 各言語の Formatter 設定

チームが生産性高くコーディング規約に従うためには Linter に加え Formatter を設定することも推奨されます。 ウォンテッドリーで利用している Formatter は以下の通りです。

* Backend
  * Ruby: 未使用
  * Golang: [gofmt](https://pkg.go.dev/cmd/gofmt)
* Frontend
  * TypeScript: [frolint (internal)](https://github.com/wantedly/frolint)
  * Swift: [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) の `.swiftformat` ファイル (リポジトリごとに配置)
  * Kotlin: JetBrains IDE標準のフォーマッター
* その他
  * Protocol Buffers: 未使用

未使用になっている言語については必ずしも利用してはいけない理由があるわけではなく、この先必要があれば導入検討が行われるべきだということに注意してください。

## コーディング規約を育てよう

コーディング規約もとい Linter 設定は一度作れば完成というものではありません。真の目的であるコードベースの品質担保と一貫性の保持を実現するためには、技術の進化に伴い、継続的に更新されていく必要があります。普段の開発でもし「あれ？」と思う点があれば、遠慮なく PR (プルリクエスト) を送ってください。規約の変更は PR 上での議論を通じて決定されます。このプロセスによりチーム全体で規約を評価し、改善することが期待されます。


# リリース・デプロイ戦略

デプロイとはソフトウェアを実際にユーザーが使えるようにする行動です。 デプロイ戦略はその工夫によってプロダクト開発の速度と品質を担保し、ユーザーへの価値提供を最大化するための取り組みです。

デプロイには単純にアプリケーションを入れ替える他にも一部のユーザーだけに機能を使えるようにしたり、 一部のリクエストのみで新しいアプリケーションをリリースするなどの様々な戦略があります。

ここでは主にコンテナ上で動作するアプリケーション・サーバーを中心にウォンテッドリーにおいて選択可能なデプロイ戦略を紹介します。

* デプロイの前提
* 基本となるデプロイ戦略
  * Rolling Update
  * Rollback
  * Migration
  * 自動デプロイの仕組み
* 応用編
  * FeatureFlag
  * Canary Deploy
* ここに書かれていること以外のデプロイ戦略

## デプロイの前提

前提として、本番環境にデプロイする前に変更の Pull Request を作成し、CI をオールグリーンにしましょう。 また開発 / QA 環境での動作確認を行い、プロジェクト/プロダクトに要求される品質に応じて QA Squad による QA テストをクリアしましょう。

## 基本となるデプロイ戦略

基本的にウォンテッドリーのアプリケーションサーバー（= Kubernetes 上のコンテナ）の多くは コードをリポジトリの main ブランチ (master ブランチ) にコミットすると CI によってテストが実行され、 テストが通った場合にのみ本番環境へデプロイが自動的に実行されます。

要素としては次のようなものがあります。

* GitHub Flow & CI/CD
* Rolling Update

また、デプロイ関連する概念として次のようなものがあります。

* Rollback
* Migration

### Rolling Update

デプロイでは展開されたアプリケーションサーバーを順番に入れ替えていきます。 そのため最大で数分程度複数のバージョンが共存する時間ができることになるため、 すべてのデプロイは後方互換性をもたせる必要があります。

### Rollback

デプロイしたコードに問題があった場合、その問題を収束させるためにまず前のバージョンのコードに戻す、Rollback 作業を検討してください。 破壊的な変更の場合は Rollback ができないこともありますが、Rollback ができる場合は Rollback が最も早く問題を収束させることができます。

問題となったコードが再度デプロイされることを防ぐために lock することを推奨しています。

```
$ kube prod deploy <前のバージョンのコミットハッシュ>
$ kube prod lock 2h
```

この操作は本番環境にデプロイ操作を行うための権限を持つ人が行う必要があります。

また Revert Pull Request を作成して問題となったコードを main ブランチ (master ブランチ) から取り除きましょう。

### Migration

GitHub Flow では main ブランチ (master ブランチ) に存在するコミットはすべてデプロイ可能であるべきです。 もちろんこれを完全に保証することは不可能ですが、自動デプロイを行っているため少なくとも最新のコミットがデプロイされて困る状態は許容できません。 したがって、データベースのマイグレーションに依存するコミットのマージはマイグレーションのあとに行われる必要があります。

そこでこれを保証する CI として [pending-migration-checker (internal)](https://github.com/wantedly/pending-migration-checker) があります。

![Pending Migration Checker の例](/files/-MhWAMxbipCI544UJN_J)

この CI が落ちている場合はデータベースのマイグレーションが必要です。 マイグレーションを `kube prod sh` などで実行して、`/check migration` とコメントすることで再度 check が実行されます。

### 自動デプロイの仕組み

基本的にウォンテッドリーでは main ブランチ (master ブランチ) に変更がコミットされてから本番環境にデプロイされるまでが自動化されています。 その仕組みを簡単に図解します。

![wantedly-cicd.drawio.png](/files/ZpcTY7Si2fB0RI76wVwR)

main (master) にコミットしたアプリケーションコードの変更は CI によってコンテナイメージにビルドされ、テストが実行されます。 テストがパスすると、Kubernetes で動いているコンテナイメージを今回新しくビルドしたコンテナイメージに差し替える処理を行い本番環境にデプロイされます。

また、 main (master) にコミットしたインフラストラクチャのコード (= Kubernetes manifest YAML) は Argo CD によって本番環境に変更がデプロイされます。

技術的な詳細は [リリース・デプロイ戦略を支える技術#Kubernetes Manifest 管理](/fields/infrastructure/deploy-strategy-implement) を参照してください。

## 応用編

プロダクト開発において一部のユーザーや少数のリクエストだけで動作を確認したい、など柔軟なデプロイが求められるシーンがあります。 ウォンテッドリーでは応用的なデプロイとして戦略と基盤がいくつか整備されています。

* FeatureFlag
* Canary Deploy

### FeatureFlag

基本的にリリースブランチを持たない我々の開発手法ではコンフリクトを避けるためにコンスタントに main ブランチ (master ブランチ) にマージしていくことが重要です。 しかしながら main ブランチ (master ブランチ) でマージされると自動でデプロイがなされるため、場合によっては変更をユーザーに見せたくないこともあります。 このような場合に FeatureFlag が有用です。 FeatureFlag は `boolean`, `string` のいずれかを返す method の返り値をリクエストごとに任意の値に変更できるウォンテッドリーの社内基盤です。 値の変更を許可しておきたい method を事前に FeatureFlag の library で wrap しておくと [Chrome Extension (internal)](https://github.com/wantedly/chrome-dev-extension) から override ができるようになります。 これを用いることでユーザーには機能を隠しておきつつ、QA 環境では簡単にその機能を顕在化させることができます。

またこれは、社内のメンバーのみに機能を先行して提供してフィードバックを集めたり、本番環境で問題を発見するために使うことができます。

詳しい方法は [リリース・デプロイ戦略を支える実装](/fields/infrastructure/deploy-strategy-implement) の FeatureFlag を参照してください。

### Canary Deploy

Canary Deploy (Canary release) は、すでにデプロイされているバージョンと新しいバージョンの間でトラフィックを分割し、 ユーザーのサブセットに展開して完全にロールアウトするアプリケーションの段階的なロールアウトです。

Canary Deploy は、新しいバージョンのアプリケーションが本番環境で問題を引き起こす可能性が拭いきれない場合に有用です。 少数のリクエストで問題があればすぐロールバックが可能なため、影響範囲を最小限に抑えることができます。

ただし、データベースのマイグレーションが伴うなど破壊的な変更の場合は注意が必要です。

実行例

```
# current branch で canary deploy を開始する
$ kube prod lock 2h
$ kube prod canary start --current
```

```
# current branch で canary deploy を終了する
kube prod canary stop
kube prod unlock
```

## ここに書かれている以外のデプロイ戦略

アプリケーションサーバー以外のデプロイは異なるデプロイ戦略の場合があります。 例えばモバイルアプリは Apple Store などのアプリストアを通してユーザーに提供されるため、ブランチ戦略から異なることがあります。

また、基本的にはデプロイ戦略はその工夫によってプロダクト開発の速度と品質を担保し、ユーザーへの価値提供を最大化するための取り組みです。 そのためシステムやアプリケーション、技術の変化によって最適なデプロイ戦略は変わってくることに注意してください。

このデプロイ戦略を実際に行ってみて問題や改善点があれば、ぜひ以下のチャンネルでフィードバックしてください。

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)

#### もっと知りたい

* [マイクロサービスでもポチポチ確認するための Kubefork | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/313884)
* <https://github.com/wantedly/kube-go> (internal)


# 上長承認が必要な作業

## はじめに

ウォンテッドリーではエンジニアに対して、多くのシステム権限が与えられます。然しながらそのシステム権限の中であれば何でもできるとは限りません。一部の作業においては上長の承認を必要とします。主には行う作業によりシステムに広域または重大な障害が発生するものや多くのユーザー様にご迷惑をおかけする可能性の高いものが該当します。 社内の承認プロセスに応じてシステム権限を操作することは非常に困難なことであり、各エンジニアが承認が必要な作業や、その承認プロセスを把握しておく事により安全なシステム開発・運用が実現されます。

## どのような作業か？

以下の 2 点が上長承認を必要とする作業として定義されています。

* 本番環境のデータを変更する作業
* ユーザーへのメールの配信

### 本番環境のデータを変更する作業

データベース管理システムの種類や、データ種類（Public / Private な情報か等）は問いません。 本番システムに対して手動でデータ変更をする事、またはデータを変更するプログラムを起動する行為が該当します。

ウォンテッドリーのシステムはすべて Immutable Infrastructure となっており、データ変更以外の目的で本番環境へコンソールログインすることはまずありません。 本番環境にコンソールログインし、データを変更する行為は上長承認が必要とシンプルに捉えても良いでしょう。

実際の作業プロセスについては、[Production でデータを編集を行うときのガイドライン (internal)](https://dev-docs.wantedly.com/production_dataedit_guildline) を参照して下さい。

### ユーザーへのメール配信

ここで言うユーザーとは Wantedly Visit 等を個人として扱う人、契約企業の管理者・採用担当者等の双方を含むすべての利用者を指します。 またメール送信すべてを指すものではなくメルマガであったり、実装したメール配信バッチやメール配信システム等からのメール送信を指します。

メールの送信については、誤配信やメール文面のガイドライン違反等の問題が発生しやすく、配信したメールを取り戻す事ができません。その特性から上長承認が必要なものとして定義されています。

社内の承認プロセスは常に変化していくものです。少しでも迷ったら周りのエンジニアや上長等に確認を取りましょう。


# アーキテクチャディシジョンレコード(ADR)

ウォンテッドリーでは、フレームワークやアーキテクチャの選定について、ADR を用いてドキュメンテーションしています。 この章では、ADRの概要とその重要性、そしてADRを記述する際のガイドラインを説明します。

## ADR とは

ADR(Architecture Decision Record) とは、アーキテクチャに関する重要な意思決定を記録するための文書です。 なぜその決定が行われたのかを説明し、将来同じ問題に直面した際にその背景を理解・議論することができるようになります。

アーキテクチャの意思決定には、システムが何を達成すべきかといった機能要件や、パフォーマンスやセキュリティ、つかいやすさといった非機能要件があります。 ADR では機能要件・非機能要件に対するどちらの解決策を記録することができます。

## なぜ ADR を書くのか

ADR を書くメリットは主に以下の2点です。

* 新たに参加したメンバーが意思決定の背景を理解し、意思決定を行ったメンバーと同じ視点で議論することができます
* 意思決定の背景を記録することにより、将来振り返った際に意思決定が適切であったかを判断し、軌道修正することができます

ウォンテッドリーでは、日々 GitHub 上で議論を行い、様々な意思決定を行っています。しかし Issue ではなく ADR というフォーマットで意思決定を記録することで、次のようなメリットを享受できます。

* 意思決定のプロセスが明確になる Issue では個人の意見とチームの意見が混ざってしまい、議論が難しくなることがあります。一方 ADR では Pull Request を通してレビューが行われるため、意思決定のプロセスが明らかになります。
* 意思決定の検索性、到達しやすさが向上する Issue は設計以外の情報も蓄積しており、意思決定を探すのが難しくなることがあります。ADR は意思決定だけが時系列に並べているため、過去の履歴を辿りやすくなっています。
* 状況把握が容易になる Issue は意思決定が当時の状況に基づいているため、現在の状況との差異がわかりにくいです。ADR は継続してメンテナンスされるため、現在の状況が反映された状態で保持されます。

## ADR になにを書くのか

ADR は上記の目的を達成するため、適切な情報を記録しておく必要があります。 一方で、継続的にメンテナンスされなければ意味がなく、書くためのコストや更新するためのコストが大きすぎると運用されなくなってしまう可能性があるため、ADR に書くべき情報は最小限に抑えるべきです。 ウォンテッドリーでは主に以下のフォーマットをもとに ADR を書くことを推奨しています。

```
# <title>

## Status
<!-- 現在のステータス。検証中、広く使ってよい、廃止、非推奨など。 -->
<!-- もちろんさまざな状況があるので、上記のように一言で表さず背景も組み合わせて書いてもよいです。 -->

## Context
<!-- このライブラリを導入する背景や、このアーキテクチャを選定するに至った背景 -->
<!-- 解決したい課題を記入する -->

## Decision
<!-- どういう決定をするか -->

## Consequences
<!-- この決断をしたことによって生じた結果。 -->
<!-- e.g. ○○がよくなった、生産性があがった、XX が難しくなった、別の課題が生まれた。 -->
```

## ADR をどこに書くのか

ADR には組織レベルとリポジトリレベルの 2 つのレベルがあります。 組織レベルの ADR は、ウォンテッドリーの全てのプロジェクトで共通の決定に関する ADR を記録します。 具体的には基盤となるようなフレームワークやアーキテクチャの選定に関する ADR です。 これらは、ウォンテッドリーの全てのプロジェクトで共通の決定であるため、wantedly/dev の docs 以下に領域ごとに配置します。(例: `wantedly/dev` の `./docs/frontend/adr`)

リポジトリレベルの ADR は、各プロジェクトで行われた意思決定を記録します。 プロジェクト内に閉じるライブラリの選定やディレクトリ構成、設計方針やコーディング規約などが該当します。 これらは、プロジェクトごとに異なるため、各プロジェクトのリポジトリに配置します。(例: 各リポジトリの `./docs/adr`)

それぞれの ADR にはディレクトリ内で一意な連番を付与して管理します。 連番は作成された順序に関わるだけで、他に特別な意味はありません。具体的には次のようなフォーマットです。 `./adr/ADR001-datetime-library.md`

## ADR をいつ、どのように書くのか

次のようなタイミングで ADR を書くことを推奨しています。

* 新規プロジェクトの始まり
* 新規ライブラリやフレームワークの導入時
* 既存のライブラリやフレームワークの廃止時
* システムの設計やアーキテクチャに関する議論を行ったとき

ADR は存在することに自体に価値があります。最初はテンプレをコピーし、埋められる情報を埋めることから初めて構いません。 Pull Request を作成したらプロジェクトのメンバーや Chapter のメンバーにレビューを依頼し、必要に応じて加筆していきます。

## ref

* <https://adr.github.io/>
* [architecture-decision-record/locales/en/templates/decision-record-template-by-michael-nygard at main · joelparkerhenderson/architecture-decision-record](https://github.com/joelparkerhenderson/architecture-decision-record/tree/main/locales/en/templates/decision-record-template-by-michael-nygard)
* [アーキテクチャの「なぜ？」を記録する！ADRってなんぞや？ - Qiita](https://qiita.com/fuubit/items/dbb22435202acbe48849)
* [O'Reilly Japan - ソフトウェアアーキテクチャの基礎](https://www.oreilly.co.jp/books/9784873119823/)

#### 話を聞きに行きたい

* Slack: [#engineering](https://wantedly.slack.com/archives/C92AZFA2D)

#### もっと知りたい

社内の ADR については以下のリポジトリを参照してください。

* [infrastructure/adr · wantedly/infrastructure - (internal only)](https://github.com/wantedly/infrastructure/tree/master/adr)
* [dev/docs/frontend/adr · wantedly/dev - (internal only)](https://github.com/wantedly/dev/tree/master/docs/frontend/adr)
* [dev/docs/backend/adr · wantedly/dev - (internal only)](https://github.com/wantedly/dev/tree/master/docs/backend/adr)


# 作業ログを残す意味

ウォンテッドリーには作業ログ（操作した Web ページのスクショ、実行したコマンド、コマンドの実行ログ、etc）を Issue や Pull Request のコメント、Slack のチャンネルに残すという習慣があります。

## 作業ログを残すメリット

* 作業内容（操作、コマンド）を記録しておくことで、あとから他の人が手順を真似できる
  * ドキュメンテーションとはまた違ったところで知識の継承を行える
  * ドキュメンテーションのきっかけとかにもなるかもしれない
* 特定の操作をしたという証跡が残る
  * 何かしら作業ミスをしたとき原因調査がしやすくなる
    * 人の記憶は信用できない
    * 記録された事実をベースに調査を行うことでより迅速に調査を進められる
* 人の解釈を挟まない事実を記録しておくことで多人数での視点（解釈）が得られてあわよくば効率的に問題を解決できる
  * 1つの事実から生まれる解釈は人それぞれ
  * 何かしらのトラブルシューティングに悩んでも作業ログを他の人に見てもらったら一瞬で解決したみたいな経験は誰しもあるはず

### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)
* Slack: [#engineering](https://wantedly.slack.com/archives/C92AZFA2D)


# 多言語化対応(i18n)

この章では多言語化対応を実施する背景やプロセス、具体的な実装方法について紹介します。

## 想定読者

* フロントエンドエンジニア
* モバイルエンジニア
* その他翻訳を依頼する人

## Why

2026 年時点でウォンテッドリーは日本語・英語の 2 ヶ国語をサポートしています。理由としてはシンガポールへの事業展開と国内利用における非日本語話者の存在が挙げられます。

そのため、新規・グロース問わずプロダクト開発を行う時には、基本的に多言語化対応を考慮する必要があります。

## 現状と課題

多言語化対応に関する問題もいくつか存在します。これらはプロダクト開発チームや Frontend Chapter によって鋭意改善中です。

* 多言語化対応が追いついていない機能がある
* フロントエンドでは推奨ライブラリである hi18n 以外の利用箇所 (古い i18n-js を使っているコードなど) が一部残っている

## プロセス

多言語化対応のプロセスは、以下のステップに分けられます。

#### 参考事例

実際に行われた翻訳プロセスの事例を置いておきます。

* [フロントエンド事例 (internal)](https://github.com/wantedly/translation-requests/issues/755)
* [モバイルアプリ事例 (internal)](https://github.com/wantedly/translation-requests/issues/764)

### 1. 日本語の文言の確定

プロダクトデザイナーと連携して翻訳の基準となる日本語の文言を確定します。 この段階でプロダクト内での一貫性を保つための用語や特殊な意味を持つ言葉を選定し、その文脈を理解することが重要です。

### 2. 翻訳依頼

翻訳は、[翻訳依頼リポジトリ](https://github.com/wantedly/translation-requests)を通じて行います。

翻訳対象となる画面の画像を添付し、次のような項目がある場合には issue に記載します。

* 特定の文言を通常とは異なる意味で伝えたい場合
* 既存の画面に文言の追加・変更があり、その中に意訳された文言が含まれる場合
* 英訳時に語順を変更したくない場合（変数として名前・アバターなどを渡す必要があり、語順が逆転することでデザイン差分が発生するなど）

### 3. 実装

ウォンテッドリーの翻訳プロセスは単なる機械翻訳ではなく、専門チーム(前述)がウォンテッドリーらしさを加味しながら作っているので数日のリードタイムが生じます。もし初めから多言語化対応した変更をリリースしたいのであれば早めに翻訳チームに依頼してください。

また、施策によってはスピード重視で日本語の文言のみ先にリリースすることもあります。

#### 実装方法

* フロントエンド実装では [hi18n](https://github.com/wantedly/hi18n) を利用します。導入手順はリポジトリの README や [使い方の紹介記事](https://www.wantedly.com/companies/wantedly/post_articles/399501) を参照してください。
* モバイルアプリ実装では、iOS/Android それぞれの OS 標準のローカライゼーション実装方法に従います。
  * [iOS 公式ドキュメント](https://developer.apple.com/documentation/xcode/localization)
  * [Android 公式ガイド](https://developer.android.com/guide/topics/resources/localization)

#### hi18n とは

[hi18n](https://github.com/wantedly/hi18n)は、TypeScript/JavaScript 向けの翻訳テキスト管理ライブラリであり、以下のような特徴を持っています。 開発の動機や設計思想、詳細な特徴などは[別記事](https://www.wantedly.com/companies/wantedly/post_articles/400195)で紹介されていますが、主な特徴は以下の通りです。

* 翻訳 ID や翻訳の引数に対して型安全性を提供します。
* React などの宣言的なフレームワークとの統合が容易です。
* Webpack などの既存の JavaScript 開発環境に自然に統合でき、ホットリロードやモジュールバンドラーの機能を利用した翻訳データの分割ロードが可能です。

## 参考

#### 話を聞きに行きたい

* Slack: [#engineering](https://app.slack.com/client/T025XTPMG/C92AZFA2D)
* Slack: [#frontend\_chapter](https://app.slack.com/client/T025XTPMG/CA46K091P)
* Slack: [#mobile\_chapter](https://app.slack.com/client/T025XTPMG/C014VN3KKB9)

#### もっと知りたい

* [wantedly/hi18n: message internationalization meets immutability and type-safety](https://github.com/wantedly/hi18n)
* [hi18n (i18nライブラリ) の紹介 (1) 設計思想と基本方針](https://www.wantedly.com/companies/wantedly/post_articles/400195)
* [hi18n (i18nライブラリ) の使い方](https://www.wantedly.com/companies/wantedly/post_articles/399501)
* [社内 Issue: I18n ライブラリを作る](https://github.com/wantedly/dx/issues/644)


# メール開発

ここでは Wantedly サービスを通じて配信されるメールの開発について解説します。 なお、説明は概要にとどめて細かい実装の詳細などは他のドキュメントで補完していくものとします。

## メールを送信する三つの方法

本ドキュメントでは 1 および 2 をスコープとします。3 については[管理画面からメールを送信する方法 (internal)](https://backstage.internal.wantedly.com/docs/default/component/wantedly/admin_user_mail_batch/#_1)を別途参照してください。

1. プロダクトコードに実装する
   * プロダクト機能として継続的に配信する場合はこちら
   * 例. 応募通知メール
   * [過去事例 (internal)](https://github.com/wantedly/visit/issues/4604)
2. 使い捨てのバッチスクリプトに実装する
   * 一度しか配信しない場合はこちら
   * 例. 新機能リリースメール
   * [過去事例 (internal)](https://github.com/wantedly/visit/issues/4713)
3. 専用管理画面から送信する
   * 非エンジニアがメールを送信する際はこちら
   * 例. メルマガ

## 特定電子メール法に関するガイドライン

プロダクト開発の施策として、企業・ユーザーにメール配信することがあります。エンジニアが各々の判断で配信してしまうと、メール配信が乱雑になったり不適切なメール内容になってしまうリスクがあります。ゆえに、Dev Branch ではルールを設けています。次のことを必ず守ってください。

* Squad 内の施策としてメール配信をする場合
  * Squad Leader の承認を Must とします
* 一つの Squad に閉じず、プロダクト横断的にメール配信をする場合 (eg. システムメンテナスなど)
  * 各 Tribe の PdM、システム責任者に事前連絡、Dev Branch Leader の承認を Must とします
* [メール配信システム](https://www.wantedly.com/admin/user_mail/batches) 経由でメール配信をする場合
  * Dev Branch Leader の承認を Must とします
* 参考
  * [迷惑メールの概要について](https://www.dekyo.or.jp/soudan/contents/taisaku/1-2.html)
  * [特定電子メールの送信の適正化等に関する法律](https://www.soumu.go.jp/main_sosiki/cybersecurity/kokumin/basic/legal/08/)

## 見るべきドキュメント

* [受信設定やレイアウトに関する仕様書 (internal)](https://backstage.internal.wantedly.com/docs/default/component/spec/mail/)
* [メール UI 実装 / メーラーの対応方針 (internal)](https://backstage.internal.wantedly.com/docs/default/component/dev/frontend/supported_mailers/)
* メール送信処理
  * [実装ドキュメント (internal)](https://backstage.internal.wantedly.com/docs/default/component/dev/frontend/emails/)
  * [QA テストの方法 (internal)](https://backstage.internal.wantedly.com/docs/default/component/dev/frontend/emails_qa/)
  * [本番送信の方法（internal）](https://backstage.internal.wantedly.com/docs/default/component/dev/frontend/send_emails_in_prod)
* 送信後の集計
  * [Looker を用いたメールの分析方法 (internal)](https://backstage.internal.wantedly.com/docs/default/component/dev/looker/mail-analysis)

## 推奨開発プロセス (未執筆)

#### 話を聞きに行きたい

* Slack: [#engineering](https://wantedly.slack.com/archives/C92AZFA2D)

#### もっと知りたい

* <https://github.com/wantedly/dev/issues/2286>
* <https://github.com/wantedly/dx/issues/456>
* <https://github.com/wantedly/dev/discussions/2288>


# 開発ツール


# kube

ここではウォンテッドリーの社内ツールである `kube` の概要について解説します。 `kube` というツール自体に着目し、Kubernetes やその運用方法については扱いません。

この章をウォンテッドリーに存在する `kube` に関するルートドキュメントと位置づけます。 したがって説明は概論にとどめて細かい利用方法などは他のドキュメントで保管していくものとします。 現在各所に散らばる `kube` に関するドキュメントをこの章に集約している段階です。

## 想定読者

主に下のような人を想定しています。

* `kube` というツールを初めて使う人
* `kube` をなんとなく使っているが全体像がよくわからない人

## kube とはなにか

`kube` は [wantedly/kube-go (internal)](https://github.com/wantedly/kube-go) で管理されているウォンテッドリーの Kubernetes クラスタのリソースを操作するためのユーティリティです。Kubernetes を操作するための標準 CLI ツールとして用意されている kubectl の機能に加え、いくつかの便利機能とサブコマンド、および権限に応じたアクセス制御の仕組みを持ちます。

このため `kube` の理解には最低限の Kubernetes の理解が必要です。 理解に不安がある場合は [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction) を参照してください。

### 責務

主に下の責務を担っています。

* `kubectl` の各種設定隠蔽
* `kubectl` の拡張
* 一般的な開発用 utility

### 実装概要

基本的には「`kube` は引数を parse して適切に変換して `kubectl` に渡している」と考えて問題ありません。

より厳密には、コマンド・機能によっては、下記のように `kubectl` の単純なラッパー以外の操作を行う場合があります。

* `kubectl` 以外の `gcloud`, `stern`, `telepresence`, `argo` などの Kubernetes 関連ツールに渡すこともある
* 各種ツールを組み合わせたり複数回呼び出すこともある
* 稀にどのツールにも依存しない機能も存在する

## セットアップ

### インストール

```bash
# latest
bash <(curl -sL https://get.wantedlyapp.com/kube)

# specific version
KUBE_VERSION=4.2.0 bash <(curl -sL https://get.wantedlyapp.com/kube)
```

### 設定

下の例のように `PATH` を通した後に `kube setup` を実行してプロンプトの指示に従ってください。

```bash
export PATH=$HOME/.wantedly/bin:$PATH
```

### コマンド体系

`kube` には大きく分けて 2 種類のコマンドが存在します。

#### クラスタ依存

ウォンテッドリーで管理する複数の Kubernetes クラスタのうち特定の一つに対して実行するコマンドです。 ミスを避けるためにクラスタを第一引数で明示する手法をとっています。 ウォンテッドリーでは 1 クラスタ 1 環境で運用しているため、`prod`, `qa` などの環境名を第一引数に指定することになります。

```bash
# 例 (sandbox / qa / prod が環境名)
kube sandbox fork
kube qa get pod
kube prod history
```

ここで提供されているコマンド群は `kube` の責務のうち主に下の 2 つを担当します。

* `kubectl` の各種設定隠蔽
* `kubectl` の拡張

#### 非クラスタ依存

一部のコマンドはクラスタに依存しない形式で存在します。 `setup`, `version`, `self-update` などの管理用のコマンドに加えて、 CI で適切な形式の Docker image を build する `ci-build` や Kubernetes manifest を生成する `generate` などのコマンドがあります。

ここで提供されているコマンド群は `kube` の責務のうち主に「一般的な開発用 utility」を担当します。

Kubernetes に関わらないコマンドも一部ありますが、下のような事情からこのような運用も一定の範囲で行っています。

* 全開発者がインストールしている
* インストール方法が簡単
* インストールが認証されている

## 機能

### Kubefork

擬似的に Kubernetes クラスタをコピーし、自分専用のデプロイ先として用いることで開発を簡単にする機能です。 詳しくは [Kubefork (internal)](https://dev-docs.wantedly.com/fork) を参照してください。

### Kubectl にできること

前述の通り `kube` は `kubectl` の wrapper として機能するため、`kubectl` で行える任意のオペレーションを実行可能です。 コマンドはすべて `kubectl` を `kube [env]` に読み替えることでそのまま実行できます。 したがって `kubectl get pods` は `kube qa get pods` や `kube sandbox get pods` などと読み替えることで実行ができます。

```bash
# kubectl
kubectl [command] [TYPE] [NAME] [flags]
# kube での読み替え
kube [env] [command] [TYPE] [NAME] [flags]
```

`kubectl` については [Overview of kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) などを参照してください。

## 知っておくと良いコマンド

`kube` はカレントディレクトリから操作対象の Namespace を決定します。 したがって、原則**操作対象のマイクロサービスのリポジトリがあるディレクトリに移動してからコマンドを実行する**のが良いでしょう。

* コマンドを実行したい: `kube <env> sh <-c, -d, branch_name> COMMAND`
  * e.g. 今のブランチで sandbox に DB migration したい `kube sandbox sh -c rails db:migrate`
  * e.g. 今デプロイされてる QA で Rails console を使いたい `kube qa sh -d rails c`
* デプロイしたい: `kube <env> deploy <-c, -d, branch_name>`
* 環境変数をセットしたい: `kube <env> dotenv set FOO=bar`
* 今動いている Pod 一覧が見たい: `kube <env> get po`
* ログが見たい: `kube <env> tail`
* クラスタをコピーし自分専用のものとして利用する: `kube <env> fork`

より詳細な使い方は [kube の使い方 (internal)](https://dev-docs.wantedly.com/deploy/kube-usage) を参照してください。

#### もっと知りたい

* [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction)
* [kube English version (internal)](https://dev-docs.wantedly.com/tools/kube)
* [Non-infra Engineers' Infrastructure at Wantedly (internal)](https://dev-docs.wantedly.com/beginners/infra)
* [wantedly/kube-go (internal)](https://github.com/wantedly/kube-go)


# Code Coverage

## 不要なコードを消すことの意義

不要なコード (以下 dead code) が存在するとプロダクトにも開発体験にも悪影響を及ぼします。 例として下のようなものがあります。

* プロダクト
  * 不要な処理によるバックエンドの速度劣化
  * 不要な asset のダウンロードによるフロントエンドの体験劣化
* 開発体験
  * build 時間の増加
  * 影響範囲推定の困難化

発見が簡単なものとそうでないものがありますが、ある dead code が他の dead code から参照されている場合、 前者の発見はより困難になります。 したがって dead code を減らしていくというアクションは継続的に行わないと簡単に対応が難しい状態になってしまいます。

## dead code 検出方法

ここでは社内で作った本番環境から取得できるログをもとにした dead code 検出方法について解説します。 2023 年 6 月時点で一部の Rails のアプリケーションのみしか対応していないので主に Rails を前提にします。

### simplecov viewer

下のフォーマットで社内のエンジニアに coverage を公開しています。

```
https://internal-only-static-files.wantedly.com/coverages/repos/<app_name>/<env>/<date>/index.html
```

* `<app_name>`: repository 名 (例: wantedly/wantedly の場合は `wantedly`)
* `<env>`: `production`, `qa`, `sandbox`, `test`
* `<date>`: `YYYY-MM-DD` のフォーマット、もしくは `latest`

Ruby における実装方法の背景上、ステートメントカバレッジのみ、つまりある行が実行されたかどうかしか記録していません。 このため、各行について少なくともその一部が実行されていることのみがわかります。

#### 例

* [wantedly/wantedly](https://internal-only-static-files.wantedly.com/coverages/repos/wantedly/production/latest/index.html)

#### 注意

この手法で検知できない dead code が存在し、また逆にここで hit していないと判定されても実際には dead code でない場合があります。 したがって実際に削除する場合には Pull Request に他の根拠を示してください。 理由としては下のようなものがあります。

* 「hit していない => 不要なコード」は成り立たない
  * 定期実行 job なども測定対象だが、測定期間内に呼び出されなかっただけの可能性もある
  * Rails の場合 initializer など測定開始前に実行されるコードは検出できない
* 「検知されている => 消すことができない」は成り立たない
  * 表示に影響のない HTML 要素や無駄な処理など仕様に影響のないコードもありえる
* 正確でない/勘違いしやすい場所がある
  * Rails の view は実装の制約上 **render されたかどうか** しか確認できない
  * ActiveRecord の scope などを 1 行で定義した場合 `scope` が定義されたこととその中の `proc` が実行されたことの区別はできない

## 実装

### 計測方法

Rails では [oneshot coverage](https://bugs.ruby-lang.org/issues/15022) を利用しています。 view についてはこの手法では計測できないため、`ActiveSupport::Notifications.subscribe` を用いて render されたかどうかのみを検知しています。 詳しい実装は wantedly/wantedly に存在する view\_coverage.rb を確認してください。

ここで検出されたものを [servicex](/fields/the-system/servicex) の `EventLogger` で BigQuery に保管しています。

### 集計方法

集計期間をできる限り長くするために次のような集計方法をとっています。

* file ごとに期間を決めてその間に各行が hit したかどうかを集計する
* 期間
  * 終了時点: `<date>` の日付
  * 開始時点: 終了時点と file の内容が変わっていない最も古い日付 (ただし最大 6 ヶ月前)

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)

#### もっと知りたい

* [不要な style 検知ツール unused-classes について (internal)](https://dev-docs.wantedly.com/frontend/unused_classes)
* [oneshot coverage を誰でも使いやすくする (internal)](https://github.com/wantedly/dx/issues/572)
* [テスト環境の Code Coverage (internal)](https://dev-docs.wantedly.com/code_coverage)


# Kubefork

この章では、ウォンテッドリーで使われている `Kubefork` の概要について説明します。

## fork とは

`Kubefork` (社内では単に `fork` と呼ばれることが多いです) とは、 任意の identifier に基づく Virtual Cluster を作成すること、と定義されています。

Virtual Cluster について説明するために、まずマイクロサービス開発においてのあるあるを説明します。

あるコンポーネントを手元で開発するためには、コンポーネントが通信する別のコンポーネントのことも考える必要があります。一般には通信部分をモックしたり、 docker-compose などを使って手元に関連するプロセスを建てるなどの戦略が考えられます。このとき、コンポーネントのことをよく知らない人は何に依存しているかはよく知らないため、開発がうまく行かないときに何が悪いのかの切り分けが難しくなってしまう、といった問題があります。

開発用クラスタにはコンポーネントが一式揃っているため、開発したコンポーネントをクラスタにデプロイする、といった方法も考えられます。しかし、この方法は他の開発者にも迷惑がかかってしまうためうまくいきません。開発用クラスタを個々人に一つづつ割り当てるという方法もありえますが、コスト的な問題で難しいでしょう。

そこで、ウォンテッドリーでは Virtual Cluster という概念を用いてマイクロサービス開発を行えるようにしています。 実際に存在する開発用クラスタに対して、仮想的なクラスタという意味で Virtual Cluster と名付けられています。 技術的には、特定のヘッダが付いている通信を特定の service にルーティングする仕組みで実現されています。

上記を説明した図が次の図です。

![virtual cluster の説明](/files/wh8Xy1m9eSeZvrrhTdA3)

開発者は 図の2つめのマイクロサービスを開発するために `A` という Virtual Cluster を作成しました。変更を加えた マイクロサービスを `A` にデプロイすることで、他の開発者に迷惑をかけずに 変更したマイクロサービスが動くかどうか、開発環境へのリクエストを使って試すことができます。

このようにして、社内の Web アプリケーションの開発では開発者それぞれ Virtual Cluster を使って開発を行っています。

## 開発手法

`fork` を用いた開発手法について次に示したあと、それぞれ説明します。

* local fork
* remote fork
* PR Preview

### local fork

次のようなコマンドを実行することで `local fork` を行うことができます。

```bash
kube <env> fork
```

`local fork` を実行すると、 Virtual Cluster が作成されたあと、[Telepresence](https://www.telepresence.io/) が起動し、 Virtual Cluster 宛のリクエストが手元に来るようになります。

リクエストが手元に来るだけでなく、クラスタ上の service へのリクエストも手元から行えるようになるので、クラスタ上で開発をしているような体験が得られます。

### remote fork

次のようなコマンドを使うと、現状開発しているブランチの Docker Image を Virtal Cluster 上にデプロイすることができます。

```bash
kube <env> fork remote -c
```

手元に開発環境がなくても、ファイルの編集と git push ができれば動作確認は remote fork で行う ということが可能です。

### PR Preview

GitHub 上で Pull Request が作成されると、その Pull Request に対応した Virtual Cluster が作成されます。また、アプリケーションに到達するためのURLがコメントされるので機能の確認などに使うことができます。

![PR Preview の例](/files/Qa6PaDVVn7oEyvRaAfuh)

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)
* Slack: [#kube-fork-feedback](https://wantedly.slack.com/archives/C013D53TUUX)

#### もっと知りたい

* [マイクロサービスでもポチポチ確認するための Kubefork](https://www.wantedly.com/companies/wantedly/post_articles/313884)
* [dev-docs/fork(internal)](https://dev-docs.wantedly.com/fork)


# ロードマップ(未執筆)


# Handbook の書き方

## 前提

ここで示すものは ToBe であり、これを満たしていないものを Handbook に含めてはいけないという性質のものではありません。 今後修正を加えていくときに少しずつこのガイドラインを満たす形式に収束していければ十分です。 そもそもこのガイドライン自体も Handbook の一部で継続的に更新されるものであるため、 ガイドラインを逸脱するドキュメントの存在を排除するのは現実的に不可能です。 ドキュメントとして存在する Handook は正しい情報が書かれている事が重要であり書き方は二の次で構いません。

## 推奨項目

### 何を書くべきか

Handbook の Web 版には空間的な制約はないため、分量そのものを気にする必要はありません。 (印刷版を作る際には、印刷版作成チームが都度内容を選定します。)

ただし、以下のような点に注意してください。

* 情報の質の観点から、なんでもかんでも書きすぎないように注意してください。 以下の基準を参考にしてください。

  * 概念のインストールとしての価値がある (業務上のプロアクティブな価値) 言い換えると、 unknown unknown を解消するために new joiner に読んでほしいような内容であること。 known unknown は人に聞いたり社内ドキュメントを探したりすればいいので、優先度は下がります。
  * または、ウォンテッドリーのやり方を知ってもらう意味で価値がある (対外的な価値)

  これらを満たさないもの (細かいリファレンスなど) は、内部向けのドキュメントに書くのがいいでしょう。
* 外部発信のルールが社内で定められています。詳細は[外部発信のルール (internal)](https://wantedly.atlassian.net/wiki/spaces/WHR/pages/3163914292)を参照してください。公開してはいけない情報や、ウォンテッドリーの見解として受け取られると問題のあるポジショニングについて触れられています。

### ファイル名

各ファイルの名前は [Web 版](https://docs.wantedly.dev) の URL の一部となります。 統一感のために単語区切りは `-` で行ってください。

### 敬体

おそらく多くの人にとって書きやすく読みやすい形式であるため敬体を推奨します。

### 個人に紐付けない

Handbook はエンジニア組織として継続的に更新していく必要があるものです。 実際にはある個人がほぼ全てを執筆した章もありますが、 より積極的な更新提案を社内から受け付けるために Handbook のすべてのテキストは個人に紐付くものではないことを共通認識として持ちます。 したがって下のような表現を避けましょう

* フロントエンドエンジニアの xx です。
* 同じチームの yy さんが作っています。
* 私の考えでは
* 個人的な意見としては
* と思います/考えます

### リンク

リンクを追加する場合は物理本で読む人がいることを意識しましょう。 物理版ではリンクは脚注で記載されますが、実際にそのURLを自分でタイプすることは稀であると考えられるので、 前後の文脈でどんな場所に書いてあるのかが想像できるようにするのが望ましいです。

### 社内リンク

この Handbook は社外にも公開されていますが、社内の人しか閲覧権限がないページへのリンクを記載することは基本的に問題ありません。 特に GitHub の repository, commit, issue, PR へのリンクを張ることは問題ありません。 ただし読者の混乱を避けるため、 `とある社内ドキュメント (internal)` のように `(internal)` という文字列をつけることを推奨します。

### 自明な表現を避ける

この Handbook はブログや研修の内容を転用した章を含んでいます。 そういった章ではコンテキストを示す「ウォンテッドリーでは」のような表現がありますが、 Handbook ではこれは前提となる情報であるため特に明示したい場合を除き削減していくことを推奨します。

### 情報の羅列 > 読み物

下の2つの目的のためあえて読み物としての読みやすさではなく局所的な読み書きのしやすさを優先します。

* 読み手が高速にエッセンスを把握できるようにする
* 書き手が修正を行うときの影響範囲を狭くする

このため情報の羅列になることを許容し、読み物としてのわかりやすさを求めません。

### フォーマット

新たなドキュメントの書くときは下のフォーマットを参考にしてみてください。

#### セクション冒頭

* 想定読者
  * 例1: フロントエンドエンジニア
  * 例2: インフラチームにタスクをお願いする人
  * 例3: モバイルアプリが関わるプロダクトを開発する人

#### セクション末尾

* 歴史 (optional)
  * これまでの技術選定の変遷などを記載すると new joiner がコンテキストをつかみやすい
* 話を聞きに行きたい
  * 誰に聞くとより詳細なことがわかるかを書く
    * Slack の channel、GitHub の team などを書くと良い
* もっと知りたい
  * 概要を掴んだ人が更に詳しく知りたいときに見るリンク集


# コントリビューター

* 青山 直樹 / @yanAoym
* 池田 伊織 / @NotFounds
* 泉 将之 / @izumin5210
* 一條 端澄 / @rerost
* 大坪 新平 / @potsbo
* 角谷 信太郎 / @kakutani
* 笠井 貴之 / @unblee
* 樫原 翔太 / @ShotaKashihara
* 唐澤 槙男 / @MaximizedOwl
* 川崎 禎紀 / @luvtechno
* 久保出 雅俊 / @kubode
* 慶山 智昭 / @tkeiyama
* 小林 直樹 / @kobayang
* 竹野 創平 / @Altech
* 千葉 知也 / @tomoasleep
* 永田 健人 / @ngtk
* 原 剛士 / @chloe463
* 原 将己 / @qnighy
* 姫野 滉盛 / @Himenon
* 大森 貴通 / @onsd
* 白鳥 昇治 / @irotoris
* 富岡 真悟 / @stomk
* 要 徳幸 / @kanamenoriyuki
* 奥山 玄稀 / @spring1018
* 水野 雅之 / @fetburner
* 渡邉 英太郎 / @eityans
* 新谷 哲平 / @euglena1215
* 永島 次朗 / @jiro
* Aubin De Traversay / @prsdta


# 社内用語集

この章では、ウォンテッドリーのエンジニア組織で使われている用語のうち新入社員がわからない可能性のあるものをリストします。 新入社員が「もしかしてこの用語は自分の知っている意味ではないかも？」と思ったときにこの章を見ることでその疑問が解決できることを目指します。 ドキュメント/ブログなどを書く場合にはこの用語集にある単語についてよく知らない人にとって混乱しないような表記を心がけるとよいでしょう。

## yashima

Wantedly People リリース時のプロジェクトが[「ヤシマ作戦」と呼ばれていた](https://www.wantedly.com/companies/wantedly/post_articles/42496)ため `yashima` という文字列が各種 repository や module の名前に残っています。

## kube

`kubectl` のラッパーとして社内でメンテナンスしているツールです。

### 参考リンク

* [プロダクト開発のための Kubernetes 入門](/fields/infrastructure/kubernetes-introduction)
* [kube](/fields/dev-tools/kube)
* [kube の使い方 (internal)](https://dev-docs.wantedly.com/deploy/kube-usage)
* [wantedly/kube-go (internal)](https://github.com/wantedly/kube-go)

## fork

一般の意味でも使われますが、社内のツールの Kubefork のこと指している場合が多くあります。

### 参考リンク

* [マイクロサービスでもポチポチ確認するための Kubefork](https://www.wantedly.com/companies/wantedly/post_articles/313884)
* [fork](/fields/dev-tools/fork)
* [fork (internal)](https://dev-docs.wantedly.com/fork)

## bq

２つの意味で使われることがあります。

* [BigQuery](https://cloud.google.com/bigquery)
  * Google Cloud サービス
* [bq.wantedly.com (internal)](https://bq.wantedly.com/)
  * 社内ツールで BigQuery に対して query を簡単に発行することができる

## wtd

`wantedly` という意味で使われます。社内最大のマザー Rails である `github.com/wantedly/wantedly` は `wtd/wtd` のように略されることがあり、 `wantedly-frontend` なども `wtd-frontend` と略されることがあります。

## blue / green

以前に Blue-Green Deployment の戦略をとっていたときの名残で各所に blue / green などの文字列が残っている場合があります。

## spec

(特に Ruby における) テストの意味。より一般的な用語としての仕様という意味でも使われますが、 プロダクトの仕様を明文化する [wantedly/spec (internal)](https://github.com/wantedly/spec) という repository を指している場合があります。

## analytics

英語小文字で analytics と書いた場合、データ同期を行う [wantedly/analytics (internal)](https://github.com/wantedly/analytics) という repository を指していることが多いです。

カタカナで「アナリティクス」と書いた場合、Visit の企業側の「アナリティクス機能」のことを指している場合があります。

## dx

ウォンテッドリーでは Developer Experience の意味で使われることが殆どで Digital Transformation の意味ではあまり用いられません。

## qa / sandbox (sb)

ウォンテッドリーでは production / qa / sandbox の3つの環境を管理しています。 staging環境や開発環境などと呼ばれるものに相当するものを参照するのに qa や sandbox が用いられます。

sandbox は sb と書かれることもあります。

## project

一般的な意味でも用いられますが、内部的なモデル名であるため Wantedly Visit の募集の意味で使われることがあります。

## job

社内でも混乱することある用語の一つです。下のような意味で使われます。

* ActiveRecord の文脈の Job model
* Kubernetes 文脈における CronJob
* Kubernetes 文脈における Job
* ActiveJob 文脈における特定のタスクを実装したクラス
* DelayedJob などを含む「非同期にタスクを実行する手法」やそれを実装したクラス/モジュール

コミュニケーションにおいては読み手書き手ともに混乱を招かない努力が必要です。

## genmon

2022年6月まで使われていた Kubernetes の認証を GitHub Access Token で行う社内認証サーバーです。 EKS への移行のタイミングで [dex に置き換えられました](https://github.com/wantedly/kube-go/blob/master/docs/auth.md)。

### 参考リンク

* [wantedly/genmon (internal)](https://github.com/wantedly/genmon)

## options / connections / notifications

一般的な意味でも使われます。しかしそれぞれ下の repository に対応する microservice を指している場合があります。

* [wantedly/options (internal)](https://github.com/wantedly/options)
* [wantedly/connections (internal)](https://github.com/wantedly/connections)
* [wantedly/notifications (internal)](https://github.com/wantedly/notifications)

## apis

一般的な意味でも使われます。しかし[wantedly/apis](https://github.com/wantedly/apis) を指すことが多くあります。

## servicex

社内で管理しているマイクロサービス共通ライブラリです。

### 参考リンク

* [マイクロサービス共通ライブラリ "servicex" の紹介](/fields/the-system/servicex)
* [wantedly/servicex (internal)](https://github.com/wantedly/servicex)
* [Visit 指標用語集 (internal)](https://docs.google.com/spreadsheets/d/1lGD5GWtyfdMiDmRi2N9No9LdgMkMD2CPYzfHXFNURAw)


# 主要な GitHub リポジトリのリスト(未執筆)


# 今後の挑戦・未解決イシュー(未執筆)


# プロダクト開発組織のバリュー(未執筆)


# 採用についての考え方(未執筆)


