TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

988

こんにちは、MLデータ部データ基盤ブロックの奥山( @pokoyakazan )です。趣味の範疇ですが、「ぽこやかざん」という名前でラジオ投稿や大喜利の大会に出たり、「下町モルモット」というコンビで週末に漫才をしたりしています。私は普段、全社データ基盤の開発・運用を担当しており、このデータ基盤はGCPのBigQuery上に構築されています。そして、データ基盤内の各テーブルは、大きく分けて以下の2種類に分類されます。 システムDBのデータやログデータなどが、特に加工されることなく連携されている一次テーブル 一次テーブルから必要なデータを使いやすい形に集計したデータマート 本記事では、後者のデータマートを集計するジョブを制御するワークフローエンジンを、DigdagからCloud Composerに移行した事例について紹介します。Cloud Composerとは、GCPにて Apache Airflow をマネージドに提供するサービスです。 cloud.google.com なお、本記事では、Cloud Composer・Apache Airflowそれぞれのバージョンは以下のものとして話を進めます。 Cloud Composer: composer-2.0.24-airflow-2.2.5 Apache Airflow: 2.2.5 そのため、記事内で参考情報リンクとして貼っている公式ドキュメントについても、こちらのバージョンのものとなります。 目次 目次 データマート集計ジョブの仕組み 各データマートの依存関係について 移行前のシステムのデータマート集計方法 各マートのSQLファイルからマート間の依存関係グラフの作成 並列に処理しても問題ないマートをまとめた集計グループを作成 各集計グループごとにマート集計を並列実行 データマート集計ジョブの課題 1つのマートの集計が失敗すると後続のグループに属する全てのマートの集計が停止する 一次テーブルの更新遅延がデータマート集計全体の遅延に繋がる 集計グループの増加に伴いデータマート集計ジョブの実行時間が長くなる DigdagからCloud Composer(Airflow)への移行 移行の契機 Airflowでのデータマート集計方法 タスクの定義 タスク間の依存関係の設定 一次テーブルの更新待ち処理追加 Cloud Composer移行によって得られた効果 Cloud Composerの運用Tips Tips1: メタデータの読み込み方法は「読み込まれるタイミング」によって使い分ける SchedulerによるDAG解析 Variablesの読み込みはTop level codeで行わない メタデータの読み込み方法の使い分け Tips2: DAG・タスクのエラーハンドリングは目的に応じてパラメータを使い分ける 1つでもタスクがエラー終了したら保守担当者に架電 エラー終了したタスクの分だけSlack通知 Tips3: Composer環境自体の外形監視を設定する Tips4: 集計遅延の検知の仕組み 採用しなかった方法1: タスクにslaパラメータを指定 採用しなかった方法2: DAG・タスクのいずれかにタイムアウト値を設定 Tips5: プライベートIP環境で構築 まとめ データマート集計ジョブの仕組み 以下の記事でもご紹介した通り、マート集計処理の実体はデータ基盤利用者が作成したSQLファイルで、全てGitHubで管理されています。 techblog.zozo.com SQLファイルにはSELECT文のみが記述されており、UPDATEやDELETEといったDMLは記載されていません。 各データマートの依存関係について あるマートが他のマートを参照している(依存関係がある)場合、集計の順番を間違えるとデータに不整合が発生してしまいます。例えば、 existing_table1 , existing_table2 という一次テーブルが存在するとし、以下のような集計クエリを持つ5つのマートを構築したい場合を考えます。 table1.sql SELECT * FROM `project.dataset.existing_table1`; table2.sql SELECT * FROM `project.dataset.existing_table2`; table3.sql SELECT * FROM `project.dataset.table2`; table4.sql SELECT * FROM `project.dataset.table1` UNION ALL SELECT * FROM `project.dataset.table3`; table5.sql SELECT * FROM `project.dataset.table3`; この場合、「 table3 の前に table2 」「 table4 の前に table1 と table3 」「 table5 の前に table3 」が集計されている必要があります。 移行前のシステムのデータマート集計方法 Digdagでマート集計する場合、以下の流れで行います。 各マートのSQLファイルからマート間の依存関係グラフの作成 並列に処理しても問題ないマートをまとめた集計グループを作成 各集計グループごとにマート集計を並列実行 各マートのSQLファイルからマート間の依存関係グラフの作成 マート間の依存関係は、各マートのSQLファイル内の、 FROM もしくは JOIN の直後にくるマート(自己参照は除く)を調べるとわかります。 FROM , JOIN の後ろに書かれているマートは、SQLを実行するマートよりも前に集計しなければなりません。そのため、「 FROM , JOIN の後ろのマート」→「SQLを実行するマート」というように依存関係グラフを作成していきます。例えば、上記の5つのSQLからマート間の依存関係グラフを作成すると以下のようになります。 これをPythonコードで実装していきます。まず、各マートのSQLファイルから以下の正規表現を使って参照先となるマートを抽出します。 (?i)(?<=FROM|JOIN)[\s \n]*`(.+?)` そして、 参照元: 参照先 という形のDictを作成します。 { 'table1' : [], # table1の依存先 'table2' : [], # table2の依存先 'table3' : [ 'table2' ], # table3の依存先 'table4' : [ 'table1' , 'table3' ], # table4の依存先 'table5' : [ 'table3' ] # table5の依存先 } 並列に処理しても問題ないマートをまとめた集計グループを作成 作成した依存関係グラフを利用し、マートの集計順序を担保したまま、可能な限り処理を並列化していきます。具体的には、並列実行しても問題のないマート同士をグループ化します。まず、親ノードがないマート群をリストに追加し、追加したマートをグラフから削除します。そして、もう一度親ノードがないマート群をリストに追加し、追加したマートをグラフから削除…というのを繰り返していきます。 結果として、以下のような集計グループのリストができあがります。 [[table1, table2], [table3], [table4, table5]] 各集計グループごとにマート集計を並列実行 各集計グループ(マートのリスト)は、リストの先頭から順番に実行可能で、同じ集計グループ内のマートの集計は並列化できます。結果として、集計の流れは以下のようになります。 [table1, table2] を並列実行 [table3] を実行 [table4, table5] を並列実行 データマート集計ジョブの課題 上記の方法で集計すると、依存関係に沿った集計順序が担保され、同じ集計グループ内では処理を並列化できます。ただし、この方法にはいくつか課題も存在します。 1つのマートの集計が失敗すると後続のグループに属する全てのマートの集計が停止する あるマートの集計ジョブがエラー終了した場合、このマートと同じ集計グループに属するマートについては、処理が並列化されているため影響を受けません。しかし、失敗したマートが属する集計グループより後のグループは、全て処理が停止してしまいます。例えば、 table1 の集計がこけた場合、以下のようになります。 [table1, table2] → table2 は実行される [table3] → 実行されない [table4, table5] → 実行されない 集計グループ単位で見ると、先頭の集計グループの処理が失敗しているので、2番目と3番目の集計グループの処理は開始されません。そのため、 table1 に依存しない table[3, 5] の集計は実行されてほしいところですが、これらのマートの集計も停止してしまいます。 一次テーブルの更新遅延がデータマート集計全体の遅延に繋がる データマートは、一次テーブルから必要なデータを使いやすい形に加工し抽出したテーブルです。そのため、一次テーブルが更新される前に、一次テーブルを参照しているマートの集計が行われるとデータの不整合が発生します。そこで、一次テーブルを参照するマートは、一次テーブルが正常に更新されるまで集計開始を待つ必要があります。さらに、集計グループ内の1つのマートのみ集計を停止させることはできないため、その場合は集計グループ自体の実行を停止(グループ内の全マートの集計を停止)させる必要があります。つまり、なんらかの理由で一次テーブルの更新が失敗・遅延すると、この一次テーブルを参照するマートが属する集計グループ内全てのマートの集計タスクが実行されません。また、一次テーブルに依存するマートの集計タスクは先頭の集計グループに属することが多いため、一次テーブルの更新遅延はマート集計ジョブ全体の大幅な遅延に繋がります。例えば、一次テーブル existing_table1 の更新が遅延している場合、以下のようになります。 [table1, table2] → 実行されず待機 [table3] → 実行されず待機 [table4, table5] → 実行されず待機 集計グループ単位で見ると、先頭の集計グループの処理が開始されないため、2番目と3番目の集計グループの処理も開始されません。そのため、 existing_table1 に依存しない table[2, 3, 5] の集計は実行されてほしいところですが、これらのマートの集計も停止してしまいます。 集計グループの増加に伴いデータマート集計ジョブの実行時間が長くなる 現在、データマートの数は900を超えており、今も日々増え続けています。さらに、各マートは複雑に依存しあっているため、マート数の増加に伴い集計グループが増えることもあります。各集計グループごとの処理は直列に実行されるため、集計グループが増加すると、マート集計ジョブ全体の実行時間も一気に増加してしまいます。 DigdagからCloud Composer(Airflow)への移行 移行の契機 Digdagではあるマートの集計が失敗した場合、失敗した集計クエリの修正対応などを行った後に、停止していた集計グループからジョブをリトライします(RETRY FAILED)。そして、リトライしたジョブの完了時間と、エラーが発生しなかった場合の普段のジョブ完了時間との差分が遅延時間となります。マート数が少ないうちは、Digdagでも特に遅延時間が大きくなることはありませんでした。むしろ、DAG(Directed Acyclic Graph)と呼ばれるタスク間に依存関係があるジョブを、YAML書式で簡潔に定義できる点でDigdagは非常に優れています。しかし、マート数が1000近くにまで増えたため、エラーが発生した際の遅延時間がとても大きくなり、上記課題の解決が急務となりました。これらの課題は、マート1つ1つに対して依存関係を定義して集計グループを作らずに集計順序を制御できれば解決が可能ですが、現状Digdagではこういった柔軟な依存関係の定義が難しいです。そこで、より柔軟にタスク間の依存関係を定義できるAirflowへの移行を検討し始めました。Airflowを実行するインフラについては、データ基盤がGCPにあるため、他GCPサービスとの連携のしやすさを考慮しCloud Composerを利用することにしました。マネージドサービスを利用することにより、運用負荷を低減することも狙いの1つです。 Airflowでのデータマート集計方法 Airflowでも、DAGと呼ばれるジョブに、タスクと呼ばれる実際の処理の内容を定義していきます。さらに、タスクの実行設定・タスク間の依存関係を追加で設定していくことで、あとは Airflow Scheduler が設定に従ってDAGを実行してくれます。実際にマート集計DAGを記述していきます。 タスクの定義 こちらのPythonコードは、毎日7:00に実行されるマート集計DAGの一部です。 import pendulum from airflow import DAG from airflow.operators.python import PythonOperator from airflow.utils.task_group import TaskGroup def _update_datamart (**kwargs): datamart_id = kwargs[ 'datamart_id' ] ''' データマート更新処理 ''' with DAG( dag_id= 'dailybatch_datamart' , start_date=pendulum.datetime( 2023 , 1 , 1 , 7 , 0 , tz= 'Asia/Tokyo' ), schedule_interval= '0 7 * * *' , catchup= False , ) as dag: datamart_ids = [ 'table1' , 'table2' , 'table3' , 'table4' , 'table5' , ] with TaskGroup(group_id= 'mart' ) as mart: for datamart_id in datamart_ids: globals ()[datamart_id] = PythonOperator( task_id=datamart_id, on_failure_callback=_failure_notify, python_callable=_update_datamart, op_kwargs={ 'datamart_id' : datamart_id}, ) datamart_ids というマート名が格納されたリストを作成し、ループで回してタスクを定義しています。マート更新処理(SQLの実行)は、全マート共通のため、全タスクで同じ関数 _update_datamart を呼び出しています(※ on_failure_callback については後述)。 タスク間の依存関係の設定 タスク定義の次は、タスク間の依存関係を設定していきます。こちらは上記「 移行前のシステムのデータマート集計方法 」内の「 各マートのSQLファイルからマート間の依存関係グラフの作成 」までは同じ手順となります。Airflowではタスク間の依存関係を >> で定義するため、正規表現を使ってSQLファイルから参照先のマートを抽出した後に、依存関係を以下のような文字列型で定義します。 'table1 >> table4' そしてこの文字列を2次元配列に格納していきます。 datamart_dependencies = [ [], # table1の依存先 [], # table2の依存先 [ 'table2 >> table3' ], # table3の依存先 [ 'table3 >> table4' , # table4の依存先 'table1 >> table4' , # table4の依存先 ], ] 最終的に、データマート集計DAGは以下のようになります(※ on_failure_callback については後述)。 import pendulum from airflow import DAG from airflow.operators.python import PythonOperator from airflow.utils.task_group import TaskGroup def _update_datamart (**kwargs): datamart_id = kwargs[ 'datamart_id' ] ''' データマート更新処理 ''' with DAG( dag_id= 'dailybatch_datamart' , start_date=pendulum.datetime( 2023 , 1 , 1 , 7 , 0 , tz= 'Asia/Tokyo' ), schedule_interval= '0 7 * * *' , catchup= False , ) as dag: datamart_ids = [ 'table1' , 'table2' , 'table3' , 'table4' , 'table5' , ] with TaskGroup(group_id= 'mart' ) as mart: for datamart_id in datamart_ids: globals ()[datamart_id] = PythonOperator( task_id=datamart_id, on_failure_callback=_failure_notify, python_callable=_update_datamart, op_kwargs={ 'datamart_id' : datamart_id}, ) # タスク間の依存関係の設定 for dependenies_of_one_mart in datamart_dependencies: for dependency in dependenies_of_one_mart: eval (dependency) other_task1 >> mart >> other_task2 また、AirflowのWeb UI上から確認できるタスク間の依存関係グラフは以下のようになります。 一次テーブルの更新待ち処理追加 Airflowを使うことで、一次テーブルが更新途中であっても「一次テーブルに依存するマートのみ待機し、依存しないマートについては影響を受けることなく集計を進める」といったことが可能になりました。これにより、一次テーブルの更新遅延がマート集計ジョブ全体の遅延に繋がる問題を解決できます。具体的には、まず一次テーブルの更新を待機する「更新待ちタスク」を定義します。更新待ちタスクは、 existing_table[1, 2] の更新時間チェックを行うBigQueryのクエリを実行し続け、更新されていることが確認できたらタスクを完了させるような内容にしています。そして、一次テーブルを参照するマートの集計タスクが、この更新待ちタスクの後にくるよう依存関係を設定します。 [ [ 'wait_existing_table1 >> table1' ], [ 'wait_existing_table2 >> table2' ], ] 結果として、AirflowのWeb UI上から確認できるタスク間の依存関係グラフはこのようになります。 これにより、一次テーブルの更新遅延による影響を極限まで小さくできました。 Cloud Composer移行によって得られた効果 Composer(Airflow)に移行することで、タスク間の依存関係を柔軟に設定できるようになりました。結果として、あるマートの集計でエラーが発生しても、そのマートと依存関係のないマートは影響を受けずに集計を進められるようになりました。例えば、 table1 の集計がエラー終了した場合、 table1 を参照する table4 のみ集計がストップし、他の table[2, 3, 5] については影響を受けることなく集計が行われます。 さらに、一次テーブルの更新待ちタスクを定義し、一次テーブルを参照するマートの集計タスクとの依存関係を設定しました。結果として、一次テーブルに依存するマートのみ更新を待ち、それ以外のマートは一次テーブルの更新タイミングに影響されることなく集計を進められるようになりました。例えば、 existing_table1 の更新処理が遅延し完了していない場合、 table1 と table4 のみ更新を待ち、他の table[2, 3, 5] については影響を受けることなく集計が行われます。 また、以下の記事で紹介した通り、DigdagではAWSのEC2インスタンス・Aurora DBを組み合わせてマート集計基盤を構築していました。 techblog.zozo.com 対して、ComposerではAirflowの環境クラスタがGKEのAutopilotモードによってマネージドに構築されるため、インフラ管理の運用負荷を下げることができました。 Cloud Composerの運用Tips 最後に、タスクの依存関係とは関係ありませんが、Composerを運用していくにあたって得た知見を記載します。Composerを運用するにあたっての参考になれば幸いです。 Tips1: メタデータの読み込み方法は「読み込まれるタイミング」によって使い分ける Airflowには、DAGの実行中に読み込みたいメタデータを、 key/value の形で、AirflowのメタデータDBに保存しておくことができる Variables という機能があります。DAG内に直接記載したくない機密情報、アクセス情報などを Variables に保存しておき、実行中のタスクから読み込むといったことが可能です。しかし、この Variables は「読み込まれるタイミング」に注意して利用する必要があります。 SchedulerによるDAG解析 Variables の注意点の前に、 Airflowのアーキテクチャ について触れておきます。Airflowのアーキテクチャは、以下のコンポーネントから成り立っています。 ジョブ実行のスケジュールを管理する Scheduler ジョブを実際に実行する Worker Web UIを提供する Web Server ここで重要なのが、 Scheduler がDAGファイル(Pythonコード)の解析を行い、 Worker がタスクを実行するということです。 Scheduler には、ジョブ実行のスケジュール管理以外にも DAGs folder と呼ばれるフォルダ内にあるPythonファイルを読み込み、DAGやタスクの設定・依存関係を解析する役割があります。そして、このDAG解析は頻繁(デフォルトでは1分に1度)に行われます。詳細についてはこちらの公式ドキュメントを参照ください。 airflow.apache.org つまり、Airflow環境で動くコードは以下の2つに大別されます。 Scheduler によって解析されるコード: Top level code Worker によって実行されるタスクのコード: Operator 内のコード 例として、以下のようなDAGについて考えてみます。 def _task1 : ''' task1の処理 ''' def _task2 : ''' task2の処理 ''' with DAG( # ~~~ ) as dag: task1 = PythonOperator( task_id= 'task1' , python_callable=_task1, ) task2 = PythonOperator( task_id= 'task2' , python_callable=_task2, ) task1 >> task2 関数 _task1 , _task2 の外のコードは、 Scheduler によって解析されるため Top level code となります。一方で、関数 _task1 , _task2 内のコードは、 Worker が実行する PythonOperator によってDAG実行時に初めて呼び出される Operator内のコード となります。ここでの大事なポイントは、 Top level code 内でサイズが大きいライブラリのインポートやDB接続といった重たい処理を行うと、DAGの解析時間が著しく遅くなってしまうという点です。 Variablesの読み込みはTop level codeで行わない Variables はAirflowのメタデータDBに保存されています。そのため、 Top level code で Variables を読み込むと、DAG解析の度に Scheduler によるメタデータDBへの接続が作成されます。結果として、DAGの解析が著しく遅くなり、 Scheduler 全体のパフォーマンスが劣化してしまいます。Composerの検証時、DAGを実行しても、タスクがQueueに停滞して実行されないという問題が発生しました。これは、DAG解析時に読み込んでいるPythonモジュール内に、 Variables を読み込む処理を記述していたことが原因と考えられます。DAGが定義されているPythonファイルだけでなく、DAG解析時に呼び出される処理も Top level code となります。そのため、DAG解析時に Variables の読み込み処理が Scheduler によって行われてしまい、パフォーマンスが劣化していました。 Top level code 内でメタデータを読み込みたい場合には2つの方法が考えられます。1つ目が、事前に環境変数として定義しておき、以下のようにDAG解析時に取得する方法です。 ENVIRONMENT = os.environ.get( 'ENVIRONMENT' ) 2つ目が、メタデータをYAMLファイルなどに保存しておき、DAG解析時に動的に読み込むようにする方法です。これらの方法で、DAG解析にかかる時間を抑えられ、 Scheduler のパフォーマンス劣化を防ぐことができます。 メタデータの読み込み方法の使い分け 以上より、メタデータの読み込み方法の方針は以下のようにしました。 Scheduler によって解析される Top level code では、環境変数やYAMLから動的に読み込む Worker によって実行される Operator内のコード では、 Variables から読み込む 具体的には、 config ディレクトリ配下に dag_parse , variables という2つのディレクトリを作成し、それぞれにメタデータが記載されたYAMLファイルを配置しています。※実際は prd , stg , dev など環境ごとに設定が分かれるため、ディレクトリ階層はもう一段深くなりますが、ここでは簡略化して記載します。 config ├── dag_parse │ ├── datamart.yml │ ├── ... └── variables ├── database.yml ├── tables.yml ├── gcp.yml ├── ... Composerでは、環境ごとにGCSバケットを持ち、このバケット内に dags , plugins , data といったフォルダが配置されています。そして、それぞれのフォルダが、各Airflowコンポーネント( data は Worker のみ)のローカル環境と同期されています。 cloud.google.com そこで、 dag_parse 配下のYAMLファイルはGCSバケットの dags フォルダへアップロードし、DAG解析時に読み込みます。一方、 variables ディレクトリ配下のYAMLファイルは、以下の流れでAirflowの Variables として登録していきます。 YAMLをJSONに変換 変換したJSONをGCSバケットの data フォルダにアップロード data フォルダと同期されている Worker 内の data フォルダから Variables を登録 2と3について補足します。Composerでは、以下のコマンドで Variables を登録できます。 gcloud composer environments run ${Composer 環境名 } variables -- import ${ 登録したいJSON } しかし、この ${登録したいJSON} は、Airflowコンポーネントのローカル環境に配置されている必要があります。そこで、GCSバケットの data フォルダにJSONをアップロードし、 Worker 内の data フォルダと同期させてから Variables として登録しています。 Variables 登録箇所のコードはこちらです。 # VARIABLES_YAML_DIR_PATH: config/variablesディレクトリのパス # VARIABLES_JSON_PATH: Variablesに登録するJSON # VARIABLES_YAML_DIR_PATH内のYAMLを1つのJSONに変換 python load_variables_from_yaml.py \ -c ${VARIABLES_YAML_DIR_PATH} \ -o ${VARIABLES_JSON_PATH} # 作成したJSONをGCSのデータフォルダにアップロード gcloud --project ${PROJECT} composer environments storage data import \ --environment ${COMPOSER_ENVIRONMENT} \ --location ${REGION} \ --source= ${VARIABLES_JSON_PATH} # データフォルダに置かれたJSONの内容をAirflow Variablesとして登録 gcloud --project ${PROJECT} composer environments run \ ${COMPOSER_ENVIRONMENT} \ --location ${REGION} \ variables -- import /home/airflow/gcs/data/variables.json load_variables_from_yaml.py の中身はこちらです。単純に YAML→PythonのDict→JSON の順で変換しているだけです。 import yaml import json import glob import argparse def load_environment_yaml_to_variables (yamls): variables_dict = {} for yaml_path in yamls: with open (yaml_path, 'r' ) as rf: loaded_variables = yaml.load(rf, Loader=yaml.SafeLoader)[project] variables_dict = dict (**variables_dict, **loaded_variables) return variables_dict if __name__ == '__main__' : parser = argparse.ArgumentParser(description= 'Argument to load variables' ) parser.add_argument( '-c' , '--conf' , required= True , help = 'Input config dir path' ) parser.add_argument( '-o' , '--output' , required= True , help = 'Output json path' ) args = parser.parse_args() config_path = args.conf output_path = args.output yamls = glob.glob(f '{config_path}/*.yml' ) variables_dict = load_environment_yaml_to_variables(yamls) with open (output_path, 'w' ) as wf: json.dump(variables_dict, wf, indent= 2 ) 上記の全ての処理を、GitHub Actionsから行うことで自動化しています。 Tips2: DAG・タスクのエラーハンドリングは目的に応じてパラメータを使い分ける 各マートの集計タスクがエラー終了した場合、目的に応じて以下2つのエラーハンドリングを行っています。 1つでもタスクがエラー終了したら保守担当者に架電 エラー終了したタスクの分だけSlack通知 1つでもタスクがエラー終了したら保守担当者に架電 マート集計ジョブ自体にエラーが発生した場合、保守担当者へ架電されるようにしています。架電の目的は「問題が起きていることを知らせること」なので、エラー終了したタスクの数によらず架電される回数は1回で十分です。そんな時は、 trigger_rule パラメータを使ったタスクを新たに定義します。デフォルトでは、タスクは上流(upstream)にある全てのタスクが成功しないと実行されません( trigger_rule=all_success )。ただし、この trigger_rule パラメータの値を変えることで、タスクの起動条件を変更できます。今回は trigger_rule に one_failed を指定することで目的が達成できます。 one_failed: At least one upstream task has failed (does not wait for all upstream tasks to be done) # 具体的な架電処理はfailure_on_call.shに記述 failure_on_call = BashOperator( task_id= 'failure_on_call' , trigger_rule= 'one_failed' , bash_command= 'failure_on_call.sh' , env={ 'message' : '[ERROR] {{ task_instance.dag_id }}' }, ) other_task1 >> mart >> other_task2 >> failure_on_call エラー終了したタスクの分だけSlack通知 一方Slack通知に関しては、エラー終了したタスクの分だけ通知が飛ぶようにしました。Slackのメッセージ内容にタスク名、エラー内容、ログへのリンクを載せることで、調査・対応をやりやすくすることが目的です。そんな時は、 on_failure_callback パラメータを使って各マートの集計タスクを定義します。こうすることで、タスクが失敗した際に、 on_failure_callback に指定した関数が呼び出されます。 def _failure_notify (context): ''' Slack通知処理 ''' with DAG( # ~~~ ) as dag: with TaskGroup(group_id= 'mart' ) as mart: for datamart_id in datamart_ids: globals ()[datamart_id] = PythonOperator( task_id=datamart_id, on_failure_callback=_failure_notify, # タスク失敗時に_failure_notifyを呼び出す python_callable=_update_datamart, op_kwargs={ 'datamart_id' : datamart_id}, ) Tips3: Composer環境自体の外形監視を設定する DAGのエラーハンドリングを設定することで、DAGの実行中の発生したエラーを検知できるようになりました。しかし、このままではComposerの環境自体に問題が発生し、そもそもDAGが実行されなくなった場合に検知できません。そこで、GCPのCloud Monitoringを使って、Composer環境自体の外型監視を入れています。Composerには、 airflow_monitoring という、環境が正常に動作しているかを監視するためのDAGが最初から用意されています。 cloud.google.com そのため、この airflow_monitoring が無事動作しているかを監視するAlert PolicyをCloud Monitoringに設定すれば、外型監視が可能となります。外型監視のAlert PolicyはTerraformで作成しており、そのTerraform定義は以下のようにしています。 resource "google_monitoring_alert_policy" "composer_healthy" { display_name = "Cloud Composer Environment Healthy" combiner = "OR" conditions { display_name = "Composer Environment Healthy" condition_threshold { aggregations { alignment_period = "60s" per_series_aligner = "ALIGN_COUNT_TRUE" cross_series_reducer = "REDUCE_SUM" group_by_fields = [ "resource.label.environment_name" , ] } comparison = "COMPARISON_LT" duration = "0s" filter = <<EOT resource.type="cloud_composer_environment" AND ( resource.labels.environment_name=$ { Composer環境名 } ) AND metric.type="composer.googleapis.com/environment/healthy" EOT threshold_value = 1 trigger { count = 1 percent = 0 } } } documentation { # ~~~ } notification_channels = [ # ~~~ ] } このように、他のGCPのマネージドサービスと組み合わせることができるのもComposerのメリットです。 Tips4: 集計遅延の検知の仕組み DAGの実行中のエラー、Composer環境自体のヘルス不良は検知できるようになりました。しかし、まだ「普段3時間で終わるマート集計DAGが6時間経っても終わっていない」といったような、集計遅延は検知できません。そこで、DAGの実行時間のSLAを定め、その時間を超えた場合にアラートを飛ばす仕組みを入れました。具体的には、監視用のDAG( sla_check_dailybatch_datamart )を新たに作成しました。この監視用DAGでは、 ExternalTaskSensor を使って、マート集計DAGを監視しています。 ExternalTaskSensor を利用するタスクでは、以下のパラメータを指定します。 external_dag_id external_task_id allowed_status そして、 external_task_id に指定したタスクの状態が allowed_status の状態へ遷移すると、 ExternalTaskSensor を利用するタスクがSuccessとなります。また、 ExternalTaskSensor は、 BaseSensorOperator というクラスを継承したクラスです。 BaseSensorOperator クラスでは、 timeout を設定でき、タスクの実行時間が指定した時間を過ぎるとエラー終了させることが可能です。マート集計DAGでは、全マートの集計完了後にSlack通知をする success_notify というタスクを定義しています。そこで、 ExternalTaskSensor から、この success_notify タスクを監視し、 timeout パラメータにSLA時間を指定しています。そうすることで、マート集計DAGの実行時間がSLA時間を超えた場合に、 ExternalTaskSensor タスクが失敗するようになります。最後に、この ExternalTaskSensor タスクが失敗した時に起動しアラートを飛ばす sla_violation_alert タスクを定義することで、集計遅延の検知が可能となります。 CHECK_DAG_ID = 'dailybatch_datamart' CHECK_TASK_ID = 'success_notify' # 対象DAGのSLA違反となる実行時間(秒) SLA_VIOLATION_TIMEOUT = 60 * 60 * 3 def _sla_violation_alert (**kwargs): ''' アラート発報処理 ''' with DAG( # ~~~ ) as dag: sla_check_dailybatch_datamart = ExternalTaskSensor( task_id= 'sla_check_dailybatch_datamart' , external_dag_id=CHECK_DAG_ID, external_task_id=CHECK_TASK_ID, timeout=SLA_VIOLATION_TIMEOUT, allowed_states=[ 'success' ], failed_states=[ 'failed' , 'skipped' ], # poke or reschedule: センサーの待機時間が長いのでスロットを解放するrescheduleを選択 mode= "reschedule" , ) sla_violation_alert = PythonOperator( task_id= 'sla_violation_alert' , trigger_rule= 'one_failed' , python_callable=_sla_violation_alert, ) sla_check_dailybatch_datamart >> sla_violation_alert 今回は ExternalTaskSensor を使う方法を採用しましたが、他に検討した集計遅延の検知方法についても記載します。 採用しなかった方法1: タスクに sla パラメータを指定 Airflowでは、各タスクのSLA時間を、 sla パラメータを使って設定できます。 airflow.apache.org 例えば、SLA時間を30秒にしたい場合、以下のように PythonOperator の引数に sla=timedelta(seconds=30) を追加します。 def sla_callback (): ''' SLA違反の際の処理 ''' with DAG( # ~~~ ) as dag: task = PythonOperator( task_id= 'task' , pythonc_collable=_collable, sla=timedelta(seconds= 30 ), sla_miss_callback=sla_callback, ) すると、30秒以上タスクが実行されるとSLA違反となり、 sla_miss_callback に設定している関数 sla_callback が呼び出されます。最後に設定した sla_callback 内からSlack通知なり架電を行うことで、集計遅延を検知できます。しかし、 sla パラメータによるSLA違反のチェックタイミングは、SLA違反したタスクの次のタスクの実行前です。そのため、遅延しているマート集計タスクが実行中の間(完了しない限り)は、遅延を検知できません。遅延しているタスクが実行中であっても、即座に遅延を検知したかったため、この sla パラメータと sla_miss_callback を組み合わせる方法は見送りました。 採用しなかった方法2: DAG・タスクのいずれかにタイムアウト値を設定 Airflowでは、DAG・タスクそれぞれに対して dagrun_timeout ・ execution_timeout といったパラメータを指定することで、実行時間のタイムアウト値を設定できます。 cloud.google.com これらのパラメータを使うと、指定した時間内にDAGまたはタスクが終わらなかった場合、強制的にエラー終了させられます。しかし、今回は集計遅延の検知さえできれば良く、実行中のタスクを強制的にエラー終了させる必要はありませんでした。また、集計遅延の原因はBigQueryジョブに時間がかかっているケースがほとんどです。遅延しているマートの集計クエリの調査・BigQueryジョブのプロファイリングを行った上で、タスクを実行させたままにするか、エラー終了させるかを運用者側で判断したいという要望もありました。そのため、この dagrun_timeout ・ execution_timeout を使う方法も見送りました。 Tips5: プライベートIP環境で構築 公共ネットワークや外部サービスからComposer環境(GKEクラスタ)へインバウンドアクセスする用途はなかったため、よりセキュアな プライベートIP環境 としてComposer環境を構築しました。また、 Cloud NAT を利用することで、外部からはアクセスできないが、外部へはアクセスできるようにしています。構築手順は以下の公式ドキュメントに沿って行っています(詳細は割愛します)。 cloud.google.com まとめ データマートの集計ジョブを制御するワークフローエンジンを、DigdagからCloud Composerに移行した事例について紹介しました。移行により、タスク間の依存関係を柔軟に設定できるようになり、1つのマートの集計エラーがマート集計ジョブ全体に及ぼす影響を小さくできました。ZOZOでは、一緒にデータ基盤を作ってくれる方を大募集しています。ご興味がある方は以下のリンクから是非ご応募ください! hrmos.co
こんにちは。ZOZO研究所の平川とML・データ部のデータサイエンスブロック2の荒木です。私たち2022年度の新卒入社メンバーは有志で社内マッチングアプリ「CLUB ZOZO」を運営しています。この記事では、興味関心が近い社員同士を自動でマッチングするアルゴリズムについてご紹介します。マッチング時のバッチ処理については推薦基盤ブロックの関口が解説していますので、興味のある方は併せてご覧ください。 qiita.com 目次 目次 CLUB ZOZOとは CLUB ZOZOを運営するにあたり解決すべき課題 ユーザ間の類似度を計るアプローチ 数理最適化を用いた偏りのないマッチング生成 ダミーデータでの推論結果 まとめ 最後に CLUB ZOZOとは CLUB ZOZOは、興味関心が近い社員同士をマッチングし、週に1回15分間のChat Timeをセッティングするサービスです。Chat Timeとは「上司」と「部下」の関係で実施される1on1ではなく、同じ興味関心を持つもの同士で純粋に会話を楽しんで欲しいという願いを込めて作った造語です。ユーザはSlackアプリ上で自分の興味関心を登録し(以下では「趣味タグ」と呼びます)待つだけでChat Timeがセッティングされるため、気軽に新しい仲間との出会いを楽しむことができます。 CLUB ZOZOは、新卒チーム開発研修で社内コミュニケーションを促進するためのツールを開発したことがきっかけとなり誕生したサービスです。2022年11月に社内リリースされ、2023年1月時点で約350名の社員が利用しています。 CLUB ZOZOを運営するにあたり解決すべき課題 CLUB ZOZOを運営する上でボトルネックとなるのがマッチングの生成です。サービスの性質上、以下の要件を満たす必要があるため手動での実施は運営側の負担が大きくなります。 共通の話題がある 同じ人とばかりマッチングしない マッチング機会が特定のユーザに偏らない そこで、私たちは機械学習と数理最適化を組み合わせたマッチングアルゴリズムを開発し、CLUB ZOZOの運営コストを大幅に削減することに成功しました。開発したアルゴリズムは、「word2vecを用いた趣味タグの類似度計算」と「数理最適化を用いた偏りのないマッチング生成」の2つの工程から成ります。 このアプローチは、ユーザのマッチング度合いに関する教師データが不要であるため、サービス立ち上げ段階で教師データが存在しない状況においても適用可能であるという利点があります。 以降では「ユーザ間の類似度を計るアプローチ」と「数理最適化を用いた偏りのないマッチング生成」について詳細を説明します。 ユーザ間の類似度を計るアプローチ 今回は、ユーザ間の類似度を「ユーザが登録しているタグの類似度」として計るアプローチを採用しました。具体的には、各ユーザのタグのすべてのペアに対して単語間の類似度を計算して、それを平均しています。 こちらの図の例では、以下の3名がそれぞれ次のタグを持っているとします。 Aさん:テニス、君の名は。 Bさん:テニス、映画 Cさん:テニス、ママ このとき、AさんとBさんは「テニス」という共通のタグを持っており、かつ「君の名は。」と「映画」という類似の趣味タグを持っているためユーザ間の類似度は55%となりました。一方で、AさんとCさんは「テニス」という共通の趣味タグを持っていますが、もう1つの「君の名は。」と「ママ」というタグはあまり似ていないためユーザ間の類似度は33.75%となります。つまり、AさんとBさんの方がより類似しているという直感をうまく反映できていると言えます。 また、単語間の類似度を計算する手法として「word2vec」を用います。word2vecは単語の意味をベクトルとして表現するモデルであり、自然言語を扱う機械学習モデルで広く用いられています。word2vecの学習には、 Wikipediaが提供している全文データ に対して、日本語用の形態素解析システムである MeCab による分かち書きを行ったデータを利用します。 しかし、このままでは辞書にない語句(=学習データに含まれない語句)や節はベクトル化できません。辞書にない語句や節の登録を禁止することも考えられますが、それではUX的にあまり嬉しくありません。そこで、もし趣味タグとして登録された文字列が辞書にない場合は、形態素解析で抽出した名詞のみを用いてベクトル化を行います。 画像のように、辞書にない文字列のペアに対して類似度の計算を実現しています。例えば、「サッカー」と「スポーツ観戦」はどちらも辞書に存在するため、そのまま類似度を計算できます。一方で、「サッカー観戦」は辞書にないタグなので、一度分かち書きをして「サッカー」と「観戦」に分割します。そして、それぞれ「スポーツ観戦」とのスコアを計算し、その平均値を類似度とします。「サッカー観戦」と「サッカー実況」のようにタグのペアのうちどちらも辞書にない語句の場合は、それぞれ分かち書きを行い、各スコアを求めて平均した結果を類似度とします。 では、もしタグに少し長めの文章が入力された場合はどうなるのでしょうか。例えば「公園で子供とサッカーをする」と「縁側で猫と日光浴をする」というペアを考えてみましょう。分かち書きを行うと、それぞれ「公園・で・子供・と・サッカー・を・する」「縁側・で・猫・と・日光浴・を・する」になります。もし、このまま各単語のベクトルを平均して類似度を計算する場合、「で・と・を」といった助詞が類似度を底上げしてしまい不自然に高いスコアが出てしまいます。実際このペアの類似度は87.5%となります。そこで、今回は形態素解析をして、品詞が名詞である単語のみを用いてベクトルを平均します。名詞だけを用いる場合、今回のペアは59.5%となり少し低めに計算されました。また、「サッカーを観戦する」「サッカー観戦」という一見すると類似度が高めに出るはずのペアも、名詞以外を用いた場合は類似度が45.5%とかなり低めに計算されてしまいます。もちろん、名詞のみを用いた場合は100%となります。 ただし、分かち書きをしても単語が辞書に存在しない場合は、スコアを0%としています。この他にも、類似度が55%未満のタグのペアは経験的にあまりふさわしいペアでないことがわかっていますので、そのペアのスコアを0%にしてペアを無効化するなど細かな調整を入れています。 数理最適化を用いた偏りのないマッチング生成 上記の方法で全ユーザの組に対してユーザの類似度が計算できたとします。ユーザの類似度が高いペアから貪欲にマッチングを成立させた場合(図の「偏り制約なし」)、「マッチング機会が特定のユーザに偏らない」という要件を満たさないマッチング結果になる場合があります。 別の方針として、一度のマッチング機会で各ユーザが1人のユーザとだけマッチングするという制約を満たしつつ、マッチングスコアの総和を最大化する解(図の「偏り制約あり」)を見つけるということが考えられます。 この問題は以下の数理最適化問題として定式化できます。 ここで、 は生成するマッチング数、 はユーザ数、 は 番目のユーザと 番目のユーザの類似度、 は 番目のユーザと 番目のユーザがマッチングするか否かを表すバイナリ変数です(以下ではマッチング変数と呼びます)。この問題は「整数線型計画問題」と呼ばれるクラスの問題であり、PuLPなどの最適化ソルバーを用いて最適解を求めることができます。 この問題の最適化対象は、マッチングが成立した組のユーザ類似度の総和です。この値は全体の効用を表現しており、値が大きいほど良いマッチングを生成できていると考えられます。1つ目の制約条件は、生成するマッチング数が指定の数と一致することを保証するための条件です。例えば、 を入力すると、10組のマッチングが生成されます。2つ目の制約条件は、一度のマッチング機会で各ユーザが1人のユーザのみとマッチングすることを保証するための条件です(以下では重複制約と呼びます)。全ユーザが同時に重複制約を満たす条件は、任意のユーザが重複制約を個別に満たすことと同値です。 以下の図では、4番目のユーザ( / Dさん)に着目して重複制約の直感的なイメージを説明します。 のユーザがマッチングするか否かを決定する際に着目すべき領域は、図中の黄色に着色されたセル(以下では対象領域と呼びます)です。マッチング変数が赤色のセルはマッチング変数の値が1になり、黒色のセルはマッチング変数の値が0になることを表すものとします。この時、4番目のユーザが重複制約を満たすための条件は、対象領域に含まれるマッチング変数の中で値が1になるものが1個(DさんはEさんとだけマッチングする / 図の中央の例)もしくは0個(Dさんは誰ともマッチングしない / 図の右端の例)の場合に限ります。実際、図の左端の例のように対象領域に含まれるマッチング変数の中で値が1になるものが2つ以上存在する場合は、Dさんは2人以上のユーザとマッチングしてしまい不適となります。 さらに、運用上は重複制約に加えて、同じユーザとばかり連続でマッチングしない制約(以下では連続制約と呼びます)を加えることも重要です。連続制約については、上記の最適化問題をソルバーに入力する前段で、対応するユーザ同士のマッチングスコア に最低値である0を代入しておくことで達成できます。同様の原理を用いると、「部署指定マッチング」などの拘束条件付きマッチングについても実現できます。 ダミーデータでの推論結果 開発したアルゴリズムがうまくマッチングできるかを確認するために、ダミーのユーザデータを用いて実際にマッチングを行いました。 左側の表は完全にランダムでマッチングされた結果であり、ほとんどのペアが出鱈目で、良いマッチング結果とは言えません。対して右側の表はword2vecによるユーザの類似度を用いたマッチング結果であり、似ている趣味タグを持っているユーザ同士のマッチングを実現できたことがわかります。各表の中央にある「Score」は、各タグの類似度を示します。 続いて、マッチングの偏りを抑止する制約の有無による結果の比較です。左側の表は先程のword2vecの結果と同じですが、同じユーザIDにそれぞれ色をつけています。ユーザID: 4, 7, 14のユーザがそれぞれ2回マッチングしており、ユーザに偏りができてしまっていることがわかります。対して右側の表はマッチングの偏りを抑止する制約を入れた結果であり、どのペアも必ず違うユーザが選ばれており、特定ユーザに偏らないマッチングを実現できたことがわかります。 まとめ 本記事では、新卒研修で開発した社内マッチングアプリ「CLUB ZOZO」のうち、ユーザ間のマッチングを行うアルゴリズムについてご紹介しました。word2vecと数理最適化の組み合わせで、興味・関心が近いユーザ同士を偏りなくマッチングするアルゴリズムを実現しました。今後は、実際にツールを利用している方から得られたフィードバックを教師データとして活用するほか、部署指定といった新機能の追加を考えています。 最後に ZOZOではファッションに関する様々な分野で、研究や開発を一緒に進めていくデータサイエンティスト・MLエンジニアを募集しています。ご興味を持たれた方は、以下のリンクからぜひご応募ください。 corp.zozo.com zozonext.com
はじめに こんにちは。ML、データ部データサイエンス2ブロックの吉本です。 ZOZOTOWNの商品には「長袖」「クルーネック」「花柄」といった、アイテムの特徴を示すタグ(アイテム特徴タグ)や「ベーシック」「モード」「結婚式」といった、アイテムに合うシーンやスタイルを表すタグ(シーン・スタイルタグ)が付与されています。これらは商品情報の登録時、ブランドさんに付与していただいているものです。 これらタグに関する課題として、タグ付与の手間、シーン・スタイルタグのタグ付与率の低さがあります。アイテム特徴タグは例えばTシャツ/カットソーカテゴリでは約50種類、シーン・スタイルタグは約130種類のタグがあり、一つ一つの商品に対してこれらの中から該当するものを選んで付与することは手間のかかる作業となります。またシーン・スタイルタグについてはZOZOTOWNに導入されてから2年弱とまだ日が浅いことから、認知度が低くタグが付与される商品の割合が小さくなっています。 そこでZOZOではタグ登録の手間の軽減、タグ付与率の向上を目標に画像認識によるタグ予測に取り組んでいます。2023年1月からは、ブランドさんが商品にタグを登録する際に、予測したタグを推薦するという仕組みとして導入されています。 また現在稼働しているシーン・スタイルタグ推薦のモデルでは、予測精度が高いタグに限って推薦を行っていますが、モデル開発では引き続き対応タグを増やすための精度の向上に取り組んでいます。こちらの取り組みでは、スタイルタグを人手でアノテーションすることによって、学習データを改善し精度の向上を目指しました。 本記事では、はじめに現時点のタグ推薦で用いられているアイテム特徴タグ、シーン・スタイルタグのモデル(商品タグによるモデル)に関して簡単に紹介したのち、スタイルタグの対応タグ数増加のための取り組み(アノテーションデータによるモデル)に関して紹介します。 はじめに ZOZOTOWNの商品タグ 商品タグによるモデル アイテム特徴タグ シーン・スタイルタグ アノテーションデータによるモデル アノテーション 予測スコアを用いた抽出 結果分析 モデル作成 評価 アノテーションデータ上での評価 リリースに向けた分析項目 分析結果 対応タグの増加、タグ推薦対象の増加 さいごに ZOZOTOWNの商品タグ ZOZOTOWNでは、見た目の特徴や素材などに関するアイテム特徴タグや、活用シーンや合うスタイルを表すシーン・スタイルタグが商品に対して付与されています。 アイテム特徴タグは「着丈」「袖丈」「柄、デザイン」といったタググループに分かれていて、その中に例えばシャツ/ブラウスカテゴリなら「着丈」の「ショート丈」「ミドル丈」「ロング丈」といったタグがあります(上図左)。アイテムのカテゴリ(シャツ/ブラウス、ニット/セーターなど)ごとに対象のタググループが決まっていて、ブランドさんはそこから選んでタグを付与できるようになっています。ユーザーさんは各タググループごとにタグを指定することで、求める特徴を持つ商品に絞り込んで検索できます。アイテム特徴タグの付与率は比較的高く、Tシャツ/カットソーの「ネック」だと約84%の商品にタグが付与されています。 シーン・スタイルタグに関しては130個ほどあり、商品ページでは「このアイテムの関連キーワード」の欄に表示されています(上図中央)。商品ページからこれらのタグをクリックすることで、当てはまる商品を検索できるようになっています(上図右)。導入されてからまだ2年弱しか経過していないということもあり、何らかのタグが1つ以上付与されている商品は全体の10%程度であり、タグがついてる商品は未だ多いとは言えない状態です。 商品タグによるモデル はじめに現在タグ推薦で利用されている、商品タグを用いて作成したモデルに関して簡単に紹介します。 アイテム特徴タグ アイテム特徴タグは多くのタグで学習に十分なデータ量がありました。このためZOZOTOWNの商品画像と、商品に付与されたアイテム特徴タグを学習データとして用いました。 モデルとしてはベースネットワークの上に、タググループごとにタグを1つ予測する層を載せたモデルを学習しました。損失関数にはSoftmax Cross Entropy Lossを用いました。ベースネットワークとしては、速度と精度ともに良い数値が報告されていた EfficientNetV2-S を用いました。 プロダクト導入に際しては社内の他部署に評価を依頼しました。各カテゴリごとに約100画像を用意し、各タググループに関して予想したタグが正しいかどうかを評価しました。この評価において、正解率が事前に定めた閾値を上回ったカテゴリ×タググループを導入することに決定しました。 シーン・スタイルタグ シーン・スタイルタグは前述の通りタグ付与率が低い状態でした。そのため商品に付与されたシーン・スタイルタグだけでは十分なデータ量を確保できないタグが多くありました。データ量の確保のため、商品説明にシーン・スタイルタグと一致する文字列が含まれる場合、そのタグが付与されているとみなして、商品に付与されたシーン・スタイルタグに加えて用いました(以後これも合わせて商品タグと呼びます)。 モデルとしてはベースネットワークの上に、各タグを付与するべきかどうかを予測する層を載せたモデルを学習しました。損失関数には負例を多く含むデータセットで高い精度が報告されている Asymmetric Loss を用いました。ベースネットワークとしてはアイテム特徴と同じくEfficientNetV2-Sを用いました。 画像認識を用いるともに、人手によりカテゴリごとに対象タグを洗い出しました。これにより精度を細かく評価できるようになるとともに、評価作業や後ほどお話するアノテーション作業の手間を軽減できました。 アイテム特徴と同じく、プロダクト導入に際しては社内の他部署に評価を依頼しました。訓練画像が十分に存在するカテゴリ×タグを評価対象とし、対象画像は各カテゴリ×タグ中で予測スコアが上位5%に入るものから50枚ずつ抽出しました。アイテム特徴と異なり、シーン・スタイルタグはそのシーンやスタイルに当てはまるか、当てはまらないかではっきりと分けることができないため、タグとしてどのくらい妥当かを4段階で評価しました。 評価の結果、評価対象としたカテゴリ×タグの約91%に当たる約300個に関して、社内で定めた閾値を上回ったため導入を決定しました。ただ評価対象としなかったものを中心に、導入を見送ったカテゴリ×タグも多く残りました。 アノテーションデータによるモデル 商品タグによるシーン・スタイルタグのモデルでは、上述のとおり精度が足りないことから導入を見送ったタグが多くありました。これらの多くは、付与数が少ないタグや商品説明文の中であまり使われない用語に関するタグでした。例えばTシャツ/カットソーのスタイルタグでは「マリン」「ロック」「セクシー」「カレッジ」「ギャル」「リゾート」の6種類のタグが該当しました。 また商品タグによるデータセットの問題点として、タグが付与されるべき商品にタグが付与されているとは限らないという問題がありました。これは精度への悪影響に加え、このデータセットを用いて信頼のできる定量評価ができない、本来タグが付与されるべき商品の割合を知ることができないといった問題につながっていました。 そこで次のステップとして、人手によるスタイルタグのアノテーションを行ったデータセットを作成することで、上記の問題を解決し対応タグ数の増加を目指しました。 アノテーション アノテーションは過去に実績のある FastLabel 社に依頼しました。65のカテゴリに対して、前述の人手で作成したカテゴリごとの対応タグリストを用いて、アノテーション対象のタグを設定しました。より主観的な要素が強いシーンタグは、アノテーション作業の効率や導入時の効果の観点から今回の対象から外し、スタイルタグのみを対象としました。 また、今回1つの画像に対して3人のアノテータにタグを付与していただきました。これは人によって各スタイルのイメージが異なり、社内で行った少量のアノテーションにおいても、人によってタグ付与の基準がばらつくという結果が出ていたためです。 アノテーション対象画像としては、約1年間分のZOZOTOWNの商品から約35万画像を抽出しました。この際に多くの商品で、1つの商品からは1カラーバリエーションの画像のみを選定するようにしました。アノテーション後、同じ商品の異なるカラーバリエーションの画像にも自動的に同じタグを付与することで、約70万の画像からなるデータセットとなりました。 予測スコアを用いた抽出 このアノテーション対象商品の選定方法ですが、今回の主目的が対応タグ数の増加にあるため、新たに対応を目指すタグが付与されそうな商品を重点的に抽出することを考えました。このために、新たに対応したいタグを対象に上述のモデルを用いて予測を行い、カテゴリ×タグごとに予測スコアが上位20%に入る商品からランダムに商品を抽出しました。トレーニングデータにおいて、この予測スコアを用いて集めた商品と、全商品からランダムに収集した画像をあわせて用いました。具体的なデータ数の目安としてはランダムに2500個(カテゴリごとにばらつきはありますが)、予測スコアを用いた分に関してはカテゴリ×タグごとに500個と設定しました。バリデーションデータ、テストデータに関しては、ZOZOTOWN上でのタグの分布と同じ分布のデータで評価するために、全量を全商品からランダムに集めたものとしました。4カテゴリに関してテスト発注、分析を行い効果が期待できそうであったため、残りの分もこの方法で収集しアノテーションを依頼しました。 結果分析 まずアノテーションデータと商品タグデータでタグ付与率を比較しました。この結果アノテーションデータでのタグ付与率の方が、商品タグデータでの付与率より4.14倍大きいことがわかりました。 3人のアノテータ間のタグの一貫性に関しても集計を行いました。カテゴリ×タグ内でタグが1人、2人、3人に付与された画像の割合を横軸に、各範囲に該当するカテゴリ×タグ数を縦軸にプロットしたヒストグラムを見ると以下のようになっています。 予測スコアを用いた収集方法の効果検証として、予測スコアを用いて収集された画像とランダムに収集された画像のそれぞれで、別々にタグ付与率を算出し比較しました。この結果、予測スコアを用いたカテゴリ×タグの30/31個において、予測スコア収集分の付与率の方が高いことがわかりました。また平均では1.7倍高いという結果になりました。予測スコア収集の対象外のタグに関しては、予測スコア収集分ではタグ付与率が下がる傾向があり、最も下がっていたTシャツ/カットソーの「エレガント」「フェミニン」「キレイめ」「ガーリー」「ナチュラル」タグでは0.65-0.77倍となっていました。 モデル作成 アノテーションデータを用いた検証をする前に、商品タグを用いたデータセットでベースネットの精度検証を行いました。前述のEfficientNetV2-Sに加えて、CoAtNet-0、CoAtNet-2、VOLO-D2を試しました。新しく試したネットワークについては keras_cv_attention_models を用いて検証しました。報告されているImageNet上での精度と同様、順当にパラメータ数が多いものほど高い精度が出るという結果になり、最も精度が高かったVOLO-D2を採用しました。 データセットはアノテーション対象データの抽出元となった約1年分の商品から作成しました。ラベルとしてはアノテーションと商品タグを別に使えるように用意しました。モデルの方でもベースネットワークの上にアノテーションタグ、商品タグそれぞれに対して別に層を用意して、別に予測、学習を行うようにしました。プロダクトで用いる際には、アノテーションで十分なラベル量があるタグはアノテーションタグの方の予測(以下アノテーションタグ予測と呼びます)を用いることにし、そうでないタグ(主にアノテーションの対象としなかったシーンタグ)に関しては商品タグの方の予測(商品タグ予測と呼びます)を用いることにしました。 損失関数には前回と同様にAssymmetric Lossを用いました。またタグを付与したアノテータ数の情報を活かすため、付与した人数×0.5で正例を重み付けしました。 評価 アノテーションデータ上での評価 評価指標としては、Precision(モデルがそのタグと予測したもののうち、正解データでアノテータ3人中2人以上がそのタグをつけた割合)、Recall(3人中2人以上がそのタグをつけたもののうち、予測できた割合)をカテゴリ×タグごとに算出し用いました。 タグを付与する/しないを決定する閾値には、タグ付与率(そのカテゴリにおいて3人中2人以上がそのタグを付与した割合)に応じた値を設定しました。 結果、Precisionは対象カテゴリ×タグの中央値では0.31、Recallは0.42でした。またPrecision算出時の正解データの条件を変更し「1人以上がそのタグを付けた割合」とすると0.72でした。 リリースに向けた分析項目 リリースに向けて以下の項目を確認しました。 タグ付与率とPrecisionの関係 アノテーションデータ上での評価と社内評価の相関 アノテーションタグ予測と商品タグ予測の相関 1はタグによる絞り込み検索の効果の参考として用いました。仮に検索対象の全商品に予測したタグを付与し、タグを指定して絞り込み検索をしたとすると、Precisionは絞り込み結果に正しくそのタグが該当する商品が含まれる割合を表します。絞り込まない場合にそのタグが含まれる割合はタグ付与率となります。タグ付与率とPrecisionを比較することで、絞り込み検索をした際にどの程度そのタグに該当する商品が増えるかを知ることができます。 2に関しては、一部のカテゴリ×タグについて商品タグを用いたモデルのときと同じく他部署の人に4段階で評価していただき、アノテーションデータでの評価が社内での評価と乖離していないかを確認しました。 3では、これまで用いてきた商品タグ予測とアノテーションタグ予測の間に大きな乖離がないかを見るため、ランキング指標を用いて確認しました。 分析結果 以下の左図が1のタグ付与率とPrecisionの関係を、カテゴリ×タグを1サンプルとしてプロットしたものになります。タグ付与率に比べてPrecisionが中央値で2.3倍となり、さらに付与率0.15以下のものに限ると4.6倍という結果になりました。絞り込みにより対象商品が数倍増えるという結果を得ることができました。 絞り込みにより対象タグの割合が増えても、タグと全く関係のない商品が検索結果の大半を占めると使われづらいことが予想されます。そこで正解データの条件を緩め、3人中1人以上がタグをつけたものとした場合のPrecisionも見てみました(下図右)。タグ付与率が0.01から0.05のものに限ると0.5、0.01以下だと0.26となりました。タグ付与率が低いカテゴリ×タグにおいても、絞り込み結果中のタグに関連する商品の割合を高くできそうなことがわかりました。 2のアノテーションデータ上での評価と社内評価の相関に関しては下図右になります。アノテーションデータでのPrecisionが高いほど、定量評価の点数が高いという傾向が見て取れます。 3の確認には、商品タグ予測とアノテーションタグ予測の間のケンドールの順位相関係数を用いました。全カテゴリ×タグのうち約97%で正の相関があることが確認できました。 対応タグの増加、タグ推薦対象の増加 これらの結果と人手でのタグリストのチェックの結果、217個のカテゴリ×タグについて、アノテーションタグ予測を用いて追加で対応することが決定しました。また現在稼働中のタグ推薦において既に対応済みのカテゴリ×タグのうち、スタイルタグの大部分になる164個に関してアノテーションタグ予測を用いることになりました。 このモデルでの改善点として、対応タグの増加の他に、推薦対象商品の増加があります。前節の商品タグを用いたデータセットでは、正確なタグ付与率を計算できませんでした。そのため社内で行った定性評価の結果などを元に、各カテゴリ×タグの中で上位10%に当たるスコアを持つ商品に対して、タグを推薦することにしていました。この方法だと、大部分の商品にそのタグが当てはまるようなカテゴリ×タグにおいては、そのタグが推薦される商品の割合が理想より小さくなってしまうといった問題がありました。今回のモデルでは、アノテーションされたタグの割合に応じて推薦のための閾値を定めたため、上記の問題を解決しタグ推薦対象の商品を増やすことができました。 さいごに 本記事では画像認識を用いた商品タグ予測に関する取り組み、特にアノテーションデータを用いた精度改善について紹介しました。今後も検索体験の向上や商品登録の手間の軽減に向けて、機械学習、システム面ともに改善を進めていきます。 ZOZOではMLエンジニアを募集しています。以下のリンクからご応募ください。 hrmos.co
こんにちは、ARやVRといったXR領域やNFTなどのWeb3領域を推進している創造開発ブロックの @ikkou です。 ZOZOCOSMEのARメイク などを担当しています。 2023年1月5日から8日の4日間にかけてラスベガスで開催された「CES 2023」に参加してきたので現地の様子をお伝えします。2020年以来、3年ぶりの現地参加となりました。 techblog.zozo.com CESとは Tech East, LVCC, Central Hall CESはCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級と言える「テクノロジーのショーケース」です。読み方は「せす」と呼ぶ方もいますが、正しくは「しーいーえす」です。 https://www.ces.tech/ 一昨年の「CES 2021」は新型コロナウイルス感染症の影響でCES史上初の完全オンライン開催となりました。そして昨年の「CES 2022」はオンラインとオフラインのハイブリッド開催ではあったものの、会期直前にオミクロン株がまん延した影響で出展や来場を見合わせる動きが目立ち、出展社数も来場者数も大幅に激減しました。さらに通常4日間の会期は1日短縮されていました。そして今年の「CES 2023」はハイブリッド開催を維持したまま、参加者数は「CES 2020」相当の活気を取り戻し、会期も4日間に戻りました。しかし、出展社数は「CES 2020」の5割程度と、まだ完全復活には時間がかかるかもしれません。 展示会場について LVCC Venetian ExpoのCES看板 CESの展示はTech East・Tech West・Tech Southという3つのエリアに大別されます。 特にLAS VEGAS CONVENTION CENTER(LVCC)から成るTech EastとVenetian Expoを中心とするTech Westにブースが多く集まっています。さらにTech Eastの中心となるLVCCはWest Hall・North Hall・Central Hall・South Hallに大別されます。 LVCC West会場の様子 「CES 2020」参加当時はまだWest Hallが建設中でしたが、「CES 2022」からWest Hallが追加されています。また、「CES 2023」ではSouth Hallが改修工事中だったため、LVCCに関してはWest Hall・North Hall・Central Hallの3エリアをまわる形となっていました。 https://www.ces.tech/exhibits/official-show-locations.aspx 今年は諸事情により会期4日目の早朝には現地を発つ必要があったため、3日間を効率よく使うためにTech EastのLVCC、Tech WestのVenetian ExpoとWynn Las Vegasに絞って朝から夜まで歩き回りました。 LVCC LoopによるLVCC各会場間の移動 LVCC West HallとLVCC West Hall Station West Hall・North Hall・Central Hallの3ホールをまわることになるLVCCですが、各会場間は徒歩でも移動できるものの、実際に歩くことなると時間がかかります。「CES 2020」当時はWest Hallが存在していなかったとはいえ、North Hall・Central Hall・South Hallを移動するだけで体力を消費していました。しかし「CES 2022」からVegas LoopのLVCCルートが開通したことで状況は大きく変わりました。 Vegas Loopはイーロン・マスク氏率いるThe Boring Company社が運営するLVCCの会場間を繋ぐ地下トンネルです。 https://www.lvcva.com/loop/ LVCC Central Station 「CES 2023」ではコンラッド系列のリゾートワールドからLVCC West Hallの北側を繋ぐ路線と、LVCC West Hallの南側、Central Hall、South Hallを繋ぐ路線の2路線が運用されていました。前者のリゾートワールドとLVCC West Hallを繋ぐ路線は1日$4.50の有料でしたが、LVCC間を繋ぐ路線は無料でした。 Vegas Loop内を走行している様子 何度かLVCC West HallとCentral Hallの移動に使いましたが、歩くと15分程度かかる道のりがわずか2分足らずで到着するので、とても便利でした。ちなみに、テスラと言えば「オートパイロット」のイメージがあるかもしれませんが、LVCC Loopでは人間のドライバーがハンドルを持っていました。ハンドルを「握る」というよりは「持っていた」ようだったので、Level 2相当のADASかもしれません。 このVegas Loopは将来的にはラスベガスの主要エリアを結ぶ交通システムとして開発されることが予定されています。同じラスベガスで毎年開催されている「AWS re:Invent」の参加者が利用する未来もあるかもしれないですね。 AR Smart GlassesとVR HMDの状況 Tech East, LVCC, Central Hall 入場待ちの様子 LVCC Central Hallには「Gaming | Metaverse | XR」とカテゴライズされた一画があります。このカテゴリー名は業界の世相を表していて、「CES 2020」当時は「AR/VR & Gaming」でした。「CES 2023」では、昨今の「メタバース」ムーブメントもあり「Metaverse」が追加され、「AR/VR」はその総称である「XR」に置き換わっていました。 私自身がCES 2023で体験したAR Smart GlassesとVR HMDの一部 「CES 2020」当時もたくさんのXRデバイスを試してきましたが、もちろん「CES 2023」でもXRデバイスを第一の目的として各社のブースをまわってきました。 「CES 2020」当時と比較するとAR Smart Glassesの勢いは少し落ち着いたように感じました。一口にAR Smart Glassesと言っても、レンズ越しに様々な情報を見せるものから、HDMIケーブルでスマートフォンやコンソールデバイスと接続して大画面で見せるもののふたつに大別できます。「CES 2022」では、後者の大画面で映像を見ることを目的としたデバイスが目立っていたように感じました。 VR HMDについては「CES 2023」で実物が初お披露目となった「Lynx R-1」や、「CES 2023」で発表された「VIVE XR Elite」などが特に注目を集めて賑わっていました。昨今のVR HMDは視界を100%バーチャルなものに置き換える、これまでのVRデバイスから、デバイス前面に搭載されているカメラで「現実世界をビデオパルスルーで見せる機能」が主流になりつつあります。これはMixed Realityの考え方です。 Lynx R-1の光学系を担当しているエンジニアの方と 「Lynx R-1」については先行してクラウドファンディングでプレッジしていることもあり、光学系を担当しているエンジニアの方と性能面などについてやり取りしました。 シャープの「VR GLASS」のプロトタイプ 日本からは「CES 2020」でパナソニックが「眼鏡型VRグラス」として発表し話題になったものが、パナソニックの100%子会社となったShiftallより「MeganeX(メガーヌエックス)」の量産機として展示されていました。また、シャープからはスマートフォンに繋ぐタイプの「VR GLASS」のプロトタイプが展示されていました。 CES 2023で見かけた触覚系デバイスの一部 VRと言うと「ゴーグルのような被るもの」を想像する方も多いかと思いますが、VRは五感に対して作用するものなので、例えば「触覚」を提示するものもVRの一部です。「CES 2023」ではこの触覚提示デバイスに相当するものが「CES 2020」当時よりも多く感じました。特に先日クラウドファンディングを終えたDiver-Xの「ContactGlove」は要注目です。 衣類の購入に際して、店頭での購入とオンラインECでの購入の違いのひとつとして生地に触れられる、という観点があります。将来的にこういった「触覚」を提示するデバイスが一般化、普及することで、メタバース空間における買い物体験が変わるかもしれません。 本記事では取り上げていないXR関連デバイスを含め、先日オンライン開催した「CES2023 オンライン報告会 ~XR関係者による本音トーク!~」のアーカイブが残っています。興味のある方はぜひご覧ください。「CES 2023」現地での交流もあり、Lynx R-1 CEOの方がフランスよりライブで繋いで日本からの質問に答えているのは貴重かもしれません。 Fashion TechとBeauty Tech XR関連以外にも面白そうなもの、業務に活かせそうなものは見てまわりましたが、その中でも特にFashion TechとBeauty Techは注視するようにしていました。これらのブースの多くはTech West, Venetian Expo 2FのLIFE STYLEエリアに出展していました。「CES 2023」では、ZOZOCOSMEのARメイクでも利用している「YouCam メイク」のパーフェクトが出展していなかった他、「CES 2020」と比べミラー系のデバイスが減ったようでした。 インスタントタトゥーマシンを提供するPrinker Fashion Tech Newsの記事でも取り上げている 「CES 2020」で人気を博していたPrinkerは「CES 2023」でも同じように大盛況でした。 Prinkerとジャグアタトゥーの比較 左の写真がPrinkerのブースで試しに印刷してもらったもので、右の写真が比較対象として現地で描いてもらったジャグアタトゥーです。どちらもインスタントタトゥーですが、Prinkerはプリントしてもらった黒以外にカラーも可能です。また、デバイスの大きさに依存するためサイズはやや小さく、耐久性は2-3日程度です。対するジャグアタトゥーは青寄りの色で、手書きのため大きさに制限はなく、ベストな発色は1週間程度です。類似のデバイスはまだそう多くありませんが、こういった可逆性のあるファッションの楽しみ方が今後どうなっていくのか楽しみです。 Koséのブース 日本からはKoséが「Maison KOSÉ銀座」で体験できる、デジタルパーソナルカラー体験「COLOR MACHINE」と関連する技術を出展していました。KoséのコスメブランドのひとつであるESPRIQUEがZOZOCOSMEのARメイクに対応しているという繋がりもあり、「COLOR MACHINE」の存在は認識していました。しかし、なかなか足を運ぶ機会がなく、奇しくも異国の地で体験することになりました。「Maison KOSÉ銀座」では体験後にパーソナルアドバイスシートを頂けるようなので、興味のある方は足を運んでみてください。 Fashion Show Las Vegas ファッション文脈ではWynn、Venetian Expoの近くにあるラスベガス最大級のショッピングモール「 Fashion Show Las Vegas 」も覗いてきました。ここでは週末にライブやファッションショーが催されるのですが、タイミングがあわず、それらは見れませんでした。 おわりに 前回同様、今回のCESは開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 世界情勢に伴うフライト価格の高騰や円安の影響を受け、海外カンファレンスへの参加コストは確実に上昇しています。実際、今回も2020年参加時と同じ航空会社・宿泊施設を利用しましたが、およそ1.5倍の金銭的コストが発生しました。CESに限らず、海外カンファレンスの参加におけるこうした金銭的コストの妥当性を説明するのは難しい側面もありますが、ことXR領域に関しては文字通り「百聞は一体験に如かず」です。現地に足を運び、その目その手で体験することに価値があると考えています。 CESの特性上、本来であれば現地でのビジネスミーティングの実施や、会食などを通してより深いやり取りをするべきですが、今回に関しては新型コロナウイルス オミクロン株の派生型「XBB.1.5」がアメリカで急速に広がっている状況を鑑みて泣く泣くそういったコミュニケーションを見送りました。そういった事情もあり、例年よりも直接的に得られているものが少ない分、より意識的に業務に活かしていきたいと考えています。 最後までご覧いただきありがとうございました。ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 現場からは以上です!
こんにちは。検索基盤部の山﨑です。検索基盤部では、検索基盤の速度改善やシステム改善だけではなく検索の精度改善にも力を入れて取り組んでいます。 検索システム改善についての過去の取り組み事例は、 こちら のリンクをご参照ください。 techblog.zozo.com また、ZOZOTOWNの検索ではElasticsearchを活用しています。Elasticsearchに関する取り組み事例は こちら のリンクをご参照ください。 techblog.zozo.com 本記事では、ZOZOTOWNで近年実施した検索の精度改善の取り組み事例を紹介します。 目次 目次 はじめに ZOZOTOWN検索の処理フロー ZOZOTOWN検索改善の方針について 商品のリランキングロジックについて 商品のリランキングロジックの概要 特徴量ロギングの導入について 今後のZOZOTOWN検索の展望 おわりに はじめに ZOZOTOWN検索の処理フロー ZOZOTOWN検索では、ユーザーが検索クエリを入力してから検索結果を出力するまで大きく3つのステップに分けられます。 Step 1: ユーザーから入力された検索クエリを受け取る。 検索クエリの支援のために検索クエリサジェスト機能があります。 サジェスト改善の取り組みについては、 ユーザーログを活用したZOZOTOWNの検索サジェスト改善 をご覧ください。 Step 2: 検索クエリの意図を解釈した結果を基にElasticsearchのクエリを作成する。 クエリ分割をQuery Segmentation、クエリの属性の引当をEntity Recognitionと呼ぶことがあります。 Step 3: 作成されたElasticsearchのクエリを基に、検索結果に表示される商品の絞り込みと並べ替えを行う。 本記事では特に以下の内容について紹介します。 全ステップに共通するZOZOTOWN検索改善の方針について Step 3の仕組みの概要と特徴量ロギングの導入について Step 2の現状と今後の展望について 次節では、全ステップに共通する改善方針について説明します。 ZOZOTOWN検索改善の方針について プロダクトを成長させるためには、良い評価指標の設計と施策の試行回数が必要です。全ての施策はA/Bテストで検証するため、各ステップでA/Bテストが容易に実施可能な状態となるようにシステム改修を進めました。 各ステップでA/Bテストが実施できる状態になった後は、評価指標の設計を進めました。評価指標の設計はZOZOTOWN全体のKGI/KPIまで関わるため、非常に難しい内容です。 私たちは、まず「良いZOZOTOWN検索とはなにか?」を議論してKPIツリーを作成しました。このKPIツリーをベースに、ガードレール指標を定めました。ガードレール指標とは、ビジネス上毀損したくない指標で、例えばZOZOTOWN全体の売上などが該当します。ガードレール指標をA/Bテストの度に計測することで、思わぬ事故を未然に防ぐ効果があります。こちらは、 A/B Testing at Scale Tutorial などで紹介されています。 ガードレール指標を決めた後は各ステップでのA/Bテストの評価指標(Overall Evaluation Criterionを略してOEC指標とも呼ばれます)の設計を進めました。 評価指標の設計に際して、 [2019 KDD-tutorial] Challenges, Best Practices and Pitfalls in Evaluating Results of Online Controlled Experiments で紹介されている下記5つの特性を参考にしています。 Sensitivity: 変更に対してどれだけ指標が変化するか Trustworthiness: 得られた指標がどれだけ信頼できるか Efficiency: 指標の計測がどれだけ効率的か Debuggability and Actionability: 指標の変化を説明できる状態か Interpretability and Directionality: 指標を改善したとき、最終的な目標値が改善するか A/Bテストの基盤と運用を整備すると、1回当たりのA/Bテストの実行コストが小さくなるため、小さな改善でも気軽にA/Bテストを実施できるようになります。また、Sensitivityの高い指標を定義できていれば、1回当たりのA/Bテスト期間も短くできます。そのため、Sensitivityが高い指標を見つけることは試行回数を増やす上でも大切です。 小さな改修を重ねてA/Bテストを繰り返すと、Sensitivityの高い指標については有意な差が計測され、機能に変化が出ていることが確認できました。 一方で、Sensitivityの低いKPIについては一度のA/Bテストで有意な差が計測されることが少ないため、小さな改善の積み重ねがKPIに正しく影響を与えているのか疑問視されていました。 この疑問に対応するため、ZOZOTOWN検索では、改善を積み重ねた現行ロジックと1年前のロジックをA/Bテストで比較するネガティブテストを行いました。結果として、現行のロジックと1年前のロジックではKPIに大きな差が観測され、小さな改善の積み重ねがKPIの改善に有効であったことが分かりました。 以上をまとめると、各ステップにおいて下記のサイクルを回しながら改善を進めています。 A/Bテストが可能な状態にシステムを改修する。 A/Bテスト後のリリース判断に用いるガードレール指標と評価指標を設計する。 A/Bテストを何度か実施してリリースを進める。 複数の改修をリリースした後に、1年前のロジックと比較してKPIの改善を確認する。 KPIと評価指標の見直しを行う。 次章では、近年特に改善が進んだStep 3の商品の並べ替え(以下リランキングと呼ぶ)について紹介します。 商品のリランキングロジックについて ZOZOTOWNのおすすめ順検索では、機械学習モデルを活用することでユーザー1人当たりの購入率を大幅に改善しました。 商品のリランキングロジックの概要 ZOZOTOWN検索では、検索したユーザーの情報と検索クエリを入力として、パーソナライズされた商品一覧の結果を返します。毎回全ての商品を機械学習モデルでランキングすると、計算コストが膨大になります。そこで、商品のランキングロジックを2つのフェーズに分けることで、計算コストを軽量化しました。 最初のフェーズでは、再現率を高めることを目的にルールベースのロジックや軽量な機械学習モデルを用いて、商品のフィルタリングを行います。 Elasticsearchでは、入力されたクエリと商品indexの各フィールドとのマッチスコアの重みを設定できます。下記のクエリ例の "brand_name^0.1" の 0.1 部分です。この重み付けを線形モデルで学習する手法を採用しています。 { " query ": { " bool ": { " must ": [ " multi_match ": { " query ": " クエリ1 ", " fields ": [ " brand_name^0.1 ", " shop_name^0.2 ", ... ] } , " multi_match ": { " query ": " クエリ2 ", " fields ": [ " brand_name^0.1 ", " shop_name^0.2 ", ... ] } ] } } , " score_mode ": " sum ", " boost_mode ": " sum " } 次のフェーズでは、適合率を高めることを目的に商品間の順序関係を学習する「ランキング学習」と呼ばれる手法の機械学習モデルを用いて、商品を並べ替えます。 フィルタリングされた全商品に対してランキング学習を走らせるのではなく、フィルタリング時のElasticsearchのスコア結果トップN件に絞ってリランキング処理を行います。リランキング処理でのランキング学習では、ElasticsearchのLearning to Rankプラグインを使用しています。詳しくは Elasticsearch Learning to Rankプラグインの使い方とポイント をご覧ください。 techblog.zozo.com また、機械学習モデルの開発にはVertex AI Pipelinesを利用しています。詳しくは Vertex AI Pipelinesによる機械学習ワークフローの自動化 をご覧ください。 techblog.zozo.com 機械学習モデルの導入前後でネガティブテストを実施することで、ユーザー当たりの商品購入率が大幅に改善することを確認しました。 次節では、機械学習モデル改善の取り組みの中でも評価指標が大きく改善した、特徴量ロギングの導入について説明します。 特徴量ロギングの導入について ZOZOTOWN検索の処理フローの概要は下記のとおりです。 Elasticsearchにリクエストした時点の特徴量をロギングしておくことは、機械学習モデルを構築する上での訓練データの作成の観点で重要です。 機械学習モデル構築の最初期は、ユーザーの行動ログ(ユーザーの商品インプレッションとクリックのログ)と訓練時点でElasticsearchに格納されている商品情報を紐付けることで訓練データを作成していました。 しかし、ZOZOTOWNの商品情報は高頻度で更新されているため、この訓練データの作成方法だと行動ログが記録された時点の商品情報と現在の商品情報が一致しないという課題がありました。そこで、ユーザーが検索するタイミングでの商品情報に基づいて計算された特徴量をログとして記録し、モデル学習の際に活用するように仕組みを整えました。結果として、上記の課題は解決され、さらに評価指標も改善しました。 なお、ZOZOTOWN検索では、特徴量ログを出力するためにLearning to rankプラグインを活用しています。しかし、特徴量ログを落とすとスコアの計算時とログの出力時に二重で特徴量の計算が走ってしまい、CPUに負荷がかかり検索のパフォーマンスが劣化してしまう課題がありました。私たちはプラグインに特徴量キャッシュ機能を実装することで、この課題に取り組みました。こちらの取り組みの詳細は、以下の記事をご覧ください。 techblog.zozo.com 今後のZOZOTOWN検索の展望 現行のZOZOTOWNの検索では、Step 2のクエリの意図解釈は辞書の引当てで実現しています。クエリの意図解釈に関しては、下記に記すような現行のZOZOTOWN検索には実装されていない機能が既にいくつも提案されています。 スペル修正 検索クエリのスペル等を修正することで、ユーザーが意図した結果を返す 例: Tシャル で検索 -> Tシャツ で検索 クエリ拡張 ユーザーが入力した検索クエリに関連するクエリを追加して検索することで、ユーザーの検索クエリに類似した結果も併せて返す 例: Tシャツ で検索 -> Tシャツ OR シャツ で検索 クエリ緩和 ユーザーが入力した検索クエリから一部のキーワードを削除して検索することで、より広範な結果を返す 例: Tシャツ 白 で検索 -> Tシャツ で検索 クエリ解釈については、下記の外部記事などで詳しく解説されています。 Daniel Tunkelang: Query Understanding 検索体験を向上する Query Understanding とは 私たちは、上記に紹介した機能を追加した下記のような処理フローを実現することで、一層ユーザーの意図に沿った検索結果を返すことを目指しています。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。今回紹介した検索技術に興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co
はじめに こんにちは、ブランドソリューション開発本部 フロントエンド部 WEAR Androidブロックの武永です。普段はファッションコーディネートアプリWEARのAndroidアプリを開発しています。 リリースノートを手動で毎回入力するのが面倒 WEARは多言語対応をしています。Google Play Consoleへアップロード後、Google Sheetsからテキストを4言語分コピーしたのち、申請画面でテキストを貼り付ける作業が面倒でした。誤って違う言語のリリース文言を記述してしまうリスクもあったので、検討した結果リリースノートもアプリと同じタイミングでアップロードすることにしました。 導入方法 今回はGitHub ActionsでRubyをセットアップし、Rubyのファイルを実行してリリースノートのテキストを取得します。それをテキストファイルとしてダウンロードします。使用するライブラリは2つあります。 1つ目はGoogle Sheetsからテキストを取得するRubyライブラリの「 google-drive-ruby 」です。Google Sheetsを操作できるライブラリはいくつかあったのですが、スター数もそれなりにあり信頼できると思い選定しました。 2つ目はGoogle Play Consoleにアプリのパッケージをアップロードするライブラリの「 gradle-play-publisher 」です。非公式ではありますが、できることの柔軟さが魅力的で選定に至りました。それでは実際に見ていきましょう。 Rubyファイルのセットアップ プロジェクト直下にGoogle Sheetsからテキストを取得する下記のGemfileとRubyファイルを追加します。 Gemfile source "https://rubygems.org" gem 'google_drive' download_release_note_text.rb require ' google_drive ' def fetch_google_sheets service_acount_key_json = { type : ' service_account ' , project_id : ENV [ " SERVICE_ACCOUNT_PROJECT_ID " ], private_key_id : ENV [ " SERVICE_ACCOUNT_PRIVATE_KEY_ID " ], private_key : ENV [ " SERVICE_ACCOUNT_PRIVATE_KEY " ].gsub( /\\ n / , "\n" ), client_email : ENV [ " SERVICE_ACCOUNT_CLIENT_EMAIL " ], client_id : " CLIENT_ID " , auth_uri : ' https://accounts.google.com/o/oauth2/auth ' , token_uri : ' https://oauth2.googleapis.com/token ' , auth_provider_x509_cert_url : ' https://www.googleapis.com/oauth2/v1/certs ' , client_x509_cert_url : ENV [ " SERVICE_ACCOUNT_CLIENT_X509_CERT_URL " ] }.to_json service_acount_key_io = StringIO .new(service_acount_key_json) session = GoogleDrive :: Session .from_service_account_key(service_acount_key_io) spreadsheet = session.spreadsheet_by_key( ENV [ " GOOGLE_SPREAD_SHEET_ID " ]) return spreadsheet end def save_metadata (spreadsheet) branchName = ENV [ " GITHUB_BRANCH_NAME " ].dup versionName = branchName.delete( " qa/ " ) LANGUAGES .each do |key, value| row = spreadsheet.worksheet_by_title(key).rows.find { |row| row[ 0 ] == versionName } path = " ./app/src/main/play/release-notes/ #{ value } /default.txt " File .open(path, mode = ' wb ' ) do |f| f.write(row[ RELEASE_NOTES_COLUMN ]) end end end LANGUAGES = { ' ja ' => ' ja-JP ' , ' zh-Hans ' => ' zh-CN ' , ' zh-Hant ' => ' zh-TW ' , ' en-US ' => ' en-US ' } RELEASE_NOTES_COLUMN = 7 spreadsheet = fetch_google_sheets save_metadata(spreadsheet) Rubyファイル上でGoogle Sheetsを認証します。その後リリースノートファイル作成の関数を呼び出しテキストファイルとして保存しています。LANGUAGESのdictionary型はGoogle Play ConsoleにアップロードできるようにGoogle Sheetsのシート名を置き換えています。ちなみにサービスアカウントで .gsub(/\\n/, "\n"), の処理を行なっている理由は、環境変数から改行コードを読み込んだ場合 \\n になるので置換しています。詳しい説明は後ほど行ないます。 次にGitHub Actions上で上記のRubyファイルを実行できるようにセットアップします。 download_release_note.yml build : runs-on : ubuntu-latest steps : - uses : actions/checkout@v3.2.0 with : ref : ${{ github.head_ref }} - uses : actions/setup-ruby@v1 with : ruby-version : 3.1 - name : Prepare bundler run : | gem install bundler bundle install --jobs 4 --retry 3 - name : Deploy Metadata run : bundle exec ruby get_release_note_text.rb env : GITHUB_BRANCH_NAME : ${{ github.head_ref }} GOOGLE_SPREAD_SHEET_ID : ${{ secrets.GOOGLE_SPREAD_SHEET_ID }} SERVICE_ACCOUNT_PROJECT_ID : ${{ secrets.SERVICE_ACCOUNT_PROJECT_ID }} SERVICE_ACCOUNT_PRIVATE_KEY_ID : ${{ secrets.SERVICE_ACCOUNT_PRIVATE_KEY_ID }} SERVICE_ACCOUNT_PRIVATE_KEY : ${{ secrets.SERVICE_ACCOUNT_PRIVATE_KEY }} SERVICE_ACCOUNT_CLIENT_EMAIL : ${{ secrets.SERVICE_ACCOUNT_CLIENT_EMAIL }} SERVICE_ACCOUNT_CLIENT_ID : ${{ secrets.SERVICE_ACCOUNT_CLIENT_ID }} SERVICE_ACCOUNT_CLIENT_X509_CERT_URL : ${{ secrets.SERVICE_ACCOUNT_CLIENT_X509_CERT_URL }} - name : Commit and Push run : | git add ./app/src/main/play/* git diff --name-only set -x git config user.name github-actions[bot] git config user.email github-actions[bot]@users.noreply.github.com git add . git commit --author=. -m 'add release note text' git push こちらはプルリクエストが作成されたタイミングで実行されるワークフローです。リリースノートを取得した後にその変更をコミットし、originにプッシュします。セキュアな情報が多いのでシークレットに変数を追加することを推奨します。 上記のプルリクエストがクローズされたらGoogle Play Consoleにパッケージをアップロードします。 internal_test_deploy.yml on : pull_request : branches : - main types : [ closed ] jobs : build-and-deploy : runs-on : ubuntu-latest steps : - name : Checkout uses : actions/checkout@v3 - name : set up JDK 11 uses : actions/setup-java@v3 with : distribution : "zulu" java-version : 11 - name : Create Empty local.properties for ci run : echo > local.properties - name : Copy CI gradle.properties run : mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name : Create Credential File run : echo '${{secrets.SERVICE_ACCOUNT_JSON}}' > ./app/service_account.json - name : Upload to Play Console run : ./gradlew publishReleaseBundle app/build.gradle //細かい導入方法は割愛しています import com.github.triplet.gradle.androidpublisher.ReleaseStatus play { serviceAccountCredentials.set(file('service_account.json')) track.set("internal") releaseStatus.set(ReleaseStatus.COMPLETED) } gradle-play-publisherはbuild.gradleに記述しアップロードをします。GitHub Actionsの実行ファイルはシンプルになります。internalは内部テスト配布です。内部テスト配布にしている理由は審査が入らないトラックなので即時にGoogle Play Consoleへ反映され確認がしやすいからです。alpha,betaは審査が入るので注意が必要です。 困ったこと Google Sheetsでリリースノートのバージョン管理を行なっているのですが、そのバージョンとトリガーになるブランチ名を一致させないといけませんでした。そこでGitHub Actionsでブランチ名を取得できる変数を使う方法を採用しました。実際に見ていきましょう。 download_release_note_text.rb def save_metadata (spreadsheet) branchName = ENV [ " GITHUB_BRANCH_NAME " ].dup versionName = branchName.delete( " qa/ " ) LANGUAGES .each do |key, value| row = spreadsheet.worksheet_by_title(key).rows.find { |row| row[ 0 ] == versionName } path = " ./app/src/main/play/release-notes/ #{ value } /default.txt " File .open(path, mode = ' wb ' ) do |f| f.write(row[ RELEASE_NOTES_COLUMN ]) end end end LANGUAGES = { ' ja ' => ' ja-JP ' , ' zh-Hans ' => ' zh-CN ' , ' zh-Hant ' => ' zh-TW ' , ' en-US ' => ' en-US ' } RELEASE_NOTES_COLUMN = 7 今回は github.head_ref を使用しています。運用としてはQA期間があり、テストが完了したらmainブランチへのマージを行ないます。そしてプルリクエストがオープンされたタイミングでワークフローが発火します。 github.head_ref を指定すると qa/1.0.0 のテキストが出力されます。そのテキストをRubyファイルに渡し、環境変数から値を取り出した後に qa/ の部分を削除し、バージョンのみを取得します。 まとめ 実際にブランチのトリガー1つでGoogle Play Consoleまでのアップロードを自動化してみたところ手動で行うことが減りました。人為的ミスも起こりにくくなり、導入して恩恵を受けました。ちなみに該当するバージョンのリリースノートが記載されていない場合はCIがエラーを起こして検知してくれるかつ、マージできないので気付けて便利です。GitHub Actions上で完結できるライブラリがあればもっとシンプルなワークフローになるので、改修コストもかからなくて良いと思いました。 最後までご覧いただきありがとうございました。ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com
ZOZO CTOブロックの @ikkou です。もうすぐ2022年も終わりますね。皆さんは師走のイベント、「アドベントカレンダー」に参加しましたか? ZOZOは例年アドベントカレンダーに参加していて、2020年は計100本、2021年は計125本、そして今年は昨年以上となる計175本の記事公開を“完走”しました。本記事ではその概要をお伝えします。 ZOZO Advent Calendar 2022 今年は計7個のカレンダーを実施したため、12/1-25の期間中、合計175本の記事を公開しました。 qiita.com 実施概要 アドベントカレンダーは「任意参加」で実施しています。エンジニア向けのSlackチャンネルで実施と参加を呼びかけ、各自で空いている日に登録する運用となっています。 公開する先はZOZO TECH BLOGだけでなく、QiitaやZenn、noteや個人のブログなど自由です。ZOZO TECH BLOGよりも気軽に書けるという観点から、QiitaやZennに書く方が多いです。 1人で複数の記事を書くこともあるので、今年は175本の記事に対して112名 *1 が参加しました。昨年は125本の記事に対して75名が参加していましたが、今年はいよいよ100名を超える参加人数となりました。アドベントカレンダーに参加するモチベーションは人それぞれですが、この参加人数の多さは特筆すべき点だと感じています。 アドベントカレンダーはアウトプットの練習に適したイベントです。ZOZOではテックブログをアウトプットの主軸に置いていますが、「まだテックブログを書く自信が無い」「テックブログに書くにはネタが小粒」のような場合に、アドベントカレンダーは良い機会です。 2022年の振り返り ZOZOのアドベントカレンダーでは例年その年を振り返る記事を公開しています。 開発組織については昨年同様、技術本部 本部長 兼 VPoEの @sonots が執筆しています。人事制度の見直しなど、昨年のZOZOとZOZOテクノロジーズの会社統合を経た「開発組織の今」を知れます。 qiita.com また、米国での「ZOZOFIT」の提供開始をはじめとする「プロダクト面の進歩」については、昨年同様「ファッションテックハイライト」を公開しています。あわせてご覧ください。 technote.zozo.com 過去のアドベントカレンダー ZOZOでは、2018年から毎年アドベントカレンダーに参加しています。 ZOZO Advent Calendar 2021 qiita.com ZOZOテクノロジーズ Advent Calendar 2020 qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2019 qiita.com qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2018 qiita.com qiita.com qiita.com 最後に ZOZOでは、プロダクト開発以外にも、今回のような外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com *1 : Qiitaアドベントカレンダーに登録している110名+2名の代理投稿
こんにちは。検索基盤部 検索基盤ブロックの佐藤( @satto_sann )です。 11月30日に ElasticOn Tokyo 2022 が行われました。今回弊社からは検索システムに関わるメンバー10名で参加して、そのうち2名が登壇しました。本記事では弊社エンジニアによる登壇の様子や気になったセッションについて紹介していきます。 目次 目次 ElasticOn Tokyoについて プログラム 全体聴講 ユーザ分科会 テクニカル分科会 ZOZOエンジニアが2名登壇しました 参加メンバーによるセッション紹介 ベクトルサーチによる関連性の追求 ベクトル入門、類似性について Elasticsearchでのベクトル検索について サーバレスアーキテクチャへの道 闇の魔術から身を守る。スピードが肝心 | Elasticでスピードを加速させるには? 最後に 番外編:会場の様子をお届け ElasticOn Tokyoについて ElasticOn Tokyo 2022は2022年11月30日に開催されました。コロナ禍の影響で、オフラインでの開催は3年ぶりでした。開催場所は恵比寿駅から徒歩5分ほどに位置するウェスティンホテル東京です。 会場の詳しい様子は、記事の最後に紹介します。 プログラム 本カンファレンスのプログラムは、午前は全体聴講、午後は分科会形式で行われました。 プログラムの詳細は下記を参照ください。 www.elasticon.com 全体聴講 午前は、Elasticsearch株式会社の代表である山賀氏の挨拶に始まり、基調講演や落合陽一氏による特別講演が目玉になっていました。Elastic社のGeneral ManagerであるMatt Riley氏の基調講演では、Elastic社のこれまでの歴史や今後の展望についてなど話がありました。 今後の展望に関しては、特にセキュリティ・オブザーバビリティ・AIという3つの分野がキーワードになっており、ただデータを蓄積するだけでなくよりパワフルにデータ活用できる環境を目指しているようでした。また、Elastic社のアジアVPであるBarrie Sheers氏によるアジア市場での展望の話では、アジアの各国にElastic社の拠点を作るほどアジアを重要視していることが紹介されていました。 ユーザ分科会 午後の部は、「ユーザー分科会」と「テクニカル分科会」の2つのブースに分かれます。参加者は気になるセッションのブースへ参加して聴講するという形式でした。 ユーザー分科会では、各ユーザー企業がElastic Stackの活用事例を発表し、参加者に知見が共有されました。 Elastic Stackの活用方法は企業によって様々でした。いずれの企業においてもElastic Stackを利用することでシステムが抱えていた大きな課題が解消されており、ソリューションのひとつとして大きな役割を担っていることが分かりました。 また、今後Elastic Stackの活用の幅をより広げていこうとしている企業も見受けられました。 テクニカル分科会 テクニカル分科会では、Elastic Stackの最新機能や実践的な利用方法が紹介されていました。ユーザー分科会と比べ、より技術的な知識の共有が際立っていました。 共通するテーマとして、膨大で複雑なデータを集約していかにそれらを活用するかが掲げられていました。また、オブザーバビリティ(可観測性)を強調するセッションも多く見受けられました。こちらのセッションについては、後ほどエンジニアがいくつか紹介するので詳細はそちらをご確認ください。 カンファレンスの後には、ネットワーキングの時間もあり、Elastic Stackを利用する様々なユーザー企業とコミュニケーションを取ることもできました。 ZOZOエンジニアが2名登壇しました ZOZOも午後のユーザー分科会に「ZOZOTOWNの商品検索におけるElasticsearch活用事例」というタイトルで登壇しました。 2名で登壇し、前半パートは検索基盤部 検索基盤ブロックの池田より発表しました。Elasticsearchを用いた商品検索のシステム構成や、ElasticsearchLTRプラグインを用いた「おすすめ順」の仕組みなどについて話をしました。 後半パートは、SRE部ECプラットフォーム基盤SREブロックの立花より「検索APIが利用するElasticsearchの運用をプロダクションレディにするための取り組み」について説明しました。 登壇後には、Elastic CloudのTerraform管理や運用面の課題に興味を持ってくださったユーザー企業様とネットワークを形成でき、ユーザー分科会の目的を達成できました。 登壇資料は以下にアップしておりますので、詳細はこちらをご参照ください。 speakerdeck.com 参加メンバーによるセッション紹介 ここからは参加メンバーから、聴講したテクニカル分科会やユーザ分科会の内容を一部紹介していきます。 ベクトルサーチによる関連性の追求 検索基盤部 検索基盤ブロックの今井です。 このセッションでは、ベクトル検索入門の話やElasticsearchでのベクトル検索の実現方法について紹介がありました。 ある単語を検索したとき、全文検索ではインデックス内に存在する文章の中から、その単語が含まれる文書を見つけます。言い換えると、文章内に単語が含まれていないと検索できないことを意味します。 一方、ベクトル検索を利用することで文章内に検索対象の単語が含まれていなくとも、単語の意図や意味を考慮して検索出来るようになります。そのほか、入力としてテキストだけでなく音声や画像も扱うことが出来たり、ドメインの特異性(ECのドメイン知識など)を学習して活用出来たりと、今まで出来ていなかった検索をすることが出来るようになります。 ベクトル入門、類似性について クエリと検索対象の文書をそれぞれベクトル化することで、ベクトルの距離が近いほど類似しているというように数学的な比較が出来るようになります。 また、ベクトルの次元数が多ければ多いほど、要素をより多く表現/比較することが出来るため精度は高くなります。ただ、その分計算量が増えてしまい処理時間がかかるようになってしまいます。次元数はElasticsearchでは最大2048次元まで対応しており、現実的なところでは300次元ほどではという話がありました。次元の上限に関しては、以下公式リファレンスにdimsパラメータの記載がありますので参照ください。 www.elastic.co ベクトルを用いた検索のために、ElasticsearchではANN(近似最近傍探索)がサポートされています。ANNではHNSWという複数レイヤーを使って近似値を出すアプローチをとっています。これにより、多少の精度とは引き換えに速度優先で検索することが出来て、大規模インデックスにおいても優れた処理速度で処理することが出来るとのことでした。 Elasticsearchでのベクトル検索について 実際にどう実装するかの話ですが、セッションでは以下の手順の紹介がありました。 学習モデルの準備 PyTorch(目的に沿ったモデル選び)→ クラスターへのモデルアップロード(eland_import_hub_model)→ KibanaのTrained Modelでアップロードモデル確認可能 ※モデルアップロードにはML機能を利用可能なライセンスが必要 データの取り込みと埋め込み(embedding)生成 ML inference Processorを用いて埋め込み ベクトルクエリ発行 クエリの埋め込みを生成(/ ml/trained_models/<model_id>/ infer)→ 生成された埋め込みをESLのknn項目に指定して_searchエンドポイントにリクエストを発行 knn項目で指定する内容は以下公式リファレンスを参照ください www.elastic.co 手順の紹介の最後には、全文検索でのスコアとベクトル近似スコアを組み合わせて利用することも可能という説明もありました。 このセッションを聴講して、テキストマッチングと組み合わせて利用することでテキストと画像を一緒に検索してスコアリングするなど検索の幅が広がり、可能性を感じました。知見はまだまだ貯まっていないと思いますので、実際に導入を検討する場合はベクトルの次元や要素の選定やベクトル検索の処理速度など実用レベルで利用するためによく検証する必要はあると思いました。 サーバレスアーキテクチャへの道 SRE部 ECプラットフォーム基盤SREブロックの大澤です。 このセッションではElasticsearchが現在アーキテクチャに至る経緯と課題から、今後予定されているアーキテクチャについて紹介されました。その中でもSREチームとして気になった内容についてピックアップして紹介します。 Elasticsearchは拡張性・耐障害性のためのShard分割、効率的なデータ管理のためのデータ階層化を経て現在のアーキテクチャに至りました。しかしながら様々な課題を抱えており、それら課題を解決するための新しいアーキテクチャが「ステートレス」です。 ここで特に注目したトピックは「データ投入と検索の分離」「オブジェクトストア」です。現在のアーキテクチャはデータ投入と検索でCPUを共有していますが、ステートレスではそれぞれ独立したCPUを使用します。それぞれの機能がCPUを占有して使えるようになることから、データ投入時のスループット向上やCPUコストの低減が期待できるようです。 SREチームでも負荷試験を行う際、データ投入処理にCPUが使われることで検索性能が低下する状況を観測しています。こうした状況に改善が見られるのではと非常に期待しています。 また、各ノードのローカルディスクに保持していたデータがデータオブジェクトになりレプリカデータを保持しなくなるようです。現在のアーキテクチャではノード拡張時にレプリカデータの作成完了を待つ必要があります。ステートレスアーキテクチャではこのようなアイドルタイムが軽減され、ノード拡張したら検索機能を直ちに使える状態になるのではとこちらも期待してます。 さらに将来的なアーキテクチャとしてサーバレスが紹介されました。定期的なバージョンアップ作業を抱えるSREチームとしては、バージョンレスといったキーワードは非常に魅力的です。 性能面・運用面共に新アーキテクチャはSREチームが抱える課題を解決してくれる可能性を感じました。機能提供された際には機能検証を行い、クラスタの乗換えを検討していきたいです。 闇の魔術から身を守る。スピードが肝心 | Elasticでスピードを加速させるには? 検索基盤部 検索基盤ブロックの佐藤( @satto_sann )です。 こちらのセッションでは、昨今のサイバー攻撃の脅威からサービスを防衛するための効率的なElastic Securityの利用方法について紹介されています。 とにかくタイトルが強烈だったので気になり参加しました。また、本カンファレンスの最後のセッションということもあり、私同様に気になって参加された方も多いのではないでしょうか? 昨今の働き方の多様化やシステムのクラウド化に伴って、社内だけ守る時代は終わり、ネットワークに混在する様々な自社のデバイスやシステムを脅威から守る必要があります。しかし、セキュリティ対策や監視に必要なログや関連するデータは複雑かつ指数関数的に増加する一方です。 インシデントが発生した際に、原因究明のためこれらのデータをシステムやデバイスごとに手作業で分析していては、被害がさらに拡大する恐れもあります。また、効率化のため個人で作成したツールやクエリで分析していては、スキルの属人化を招いてしまいます。そこで紹介されているのが、Elastic Securityです。 Elastic Securityは以下の機能を提供しています。 オープン&統合 セキュリティソリューションを1つの環境に閉じ込めず、再利用できるものは他の環境でも利用可能 セキュリティソリューションをブラックボックス化しない 検知ルールもオープンになっているので、アラート発生理由がわかる ネイティブプロテクション MITRE ATT&CKというサイバー攻撃の流れと手法を体系化したフレームワークやMLモデルを利用 どんなインシデントが起きていて、その原因や発生場所といった断片的なイベントを繋げた一貫した情報を提供 未来のスケールに他対応する設計 大量のデータ登録や分析が可能 今後Elastic Cloudに導入が検討されているサーバーレスの新アーキテクチャが利用できれば、よりインフラ側を意識しなくても良くなる インサイトの自動化 インシデントが起きた際の情報収集やトリアージ等の作業を自動化 分析者の負担を軽減 ハイブリッドクラウド マルチクラウドの利用が可能 マルチクラウドでデータを貯めていてもKibanaのフェデレーション機能で横断的に検索可能 発表を聞く限りElastic Cloudにデータを集約することで、オブザーバビリティが向上して、インシデント発生時に検知から解決までを一貫して効率よく対応できそうでした。 今回のカンファレンスでは特に、オブザーバビリティが強調されていたので、このセッションでもその有用性が紹介された形です。 最後にデモでは、Kibana上からElastic Securityを操作しながら上記機能を具体例と共に紹介していました。 例えば、クラウド上にあるKubernetesの設定が不足していてセキュリティの脆弱性があった場合、Elastic Securityではそれらを分析して、87%良いといったように良し悪しを可視化します。 他にもホストごとにセキュリティスコアを算出して可視化するEntity Analyticsダッシュボード。時系列ごとにどのようなアクションがされたか可視化、またその詳細の深掘りやアラート原因の可視化などをデモで紹介していました。 これらダッシュボードの詳細については下記公式ドキュメントを参照ください。 www.elastic.co セッションを聞いた感想としては、インシデント発生時の原因の分析やトリアージといった分析者の負担が特に大きい部分をElastic Securityでサポートしてもらう。これにより、迅速な対応が可能となり、被害も従来より抑えられそうだなと感じました。 また、このようなセキュリティ対策や対応は属人化も問題になるので、1つのサービスで完結できるとそういった課題も解決でき便利に感じました。 最後に 数年ぶりのオフラインイベント参加でしたが、現地でしか得られない最新情報を聞けただけではなく、合間の休憩時間でユーザー企業様と検索について意見交換できたのでとても有意義な時間を過ごせました。オンラインにはない良さを改めて感じました。 また、ZOZOではセミナー・カンファレンスへの参加を支援する福利厚生があり、カンファレンス参加チケット・宿泊費・交通費などは全て会社に負担してもらっています。今回参加メンバーが10人でカンファレンスの中でもトップレベルの大所帯だったと思いますが、例外なく福利厚生を受けられました。 ZOZOでは一緒に働く検索エンジニアを募集していますので、興味のある方は以下リンクからぜひご応募ください。 hrmos.co hrmos.co 番外編:会場の様子をお届け 検索基盤部 検索基盤ブロックの可児( @KanixT )です。 最後に会場の様子を簡単にご紹介いたします。 朝9時から開場だったため、朝早く遠方から参加された方や朝食を食べ忘れた方向けに軽食が用意されていました。 続いてElastic製品に触れられるブースです。 Kibanaなどのアイコンと一緒に写真が撮れるフォトブースもありました。 お昼はビュッフェ形式でお寿司やカレーなどさまざまなお料理が並びました。その中でもデザートのスイーツはとても美味しかったです。 セッション終了後には懇親会が催され、登壇者の方やElastic社の様々な方とお話させていただきました。
こんにちは。SRE部 ECプラットフォーム基盤SREブロックの高塚です。 11/28〜12/2に開催されたAWS re:Invent 2022に、ZOZOのエンジニア10名が参加しました。 アドベントカレンダーの1日目 ではHave Funイベントなどを紹介しましたが、この記事では現地の様子とセッションについてレポートします! AWS re:Invent 2022とは 現地の様子 セッション紹介 おわりに AWS re:Invent 2022とは re:Invent はAWS最大のカンファレンスです。2012年から毎年ラスベガスで開催されており、今年で11回目を迎えます。 2020年はオンライン開催のみで、2021年はマスク着用での開催でしたが、今年はマスクの着用義務がなくなり、いわば3年ぶりの通常開催でした。 今年も多くの新サービスや新機能が発表され、5日間で1500以上のセッションが行われています。セッションにはいくつかの形式があります。 形式 説明 Keynote 基調講演 Breakout Session 講義形式。登壇者はAWSの中の人、カスタマー、パートナーなど様々 Chalk Talk 登壇者と参加者がディスカッションする大学の講義のような形式 Workshop 参加者で少人数のグループを組み、テーマに沿って手を動かす形式 Gamified Learning ゲーム形式の参加型コンテンツ。GameDayやJamなど 現地の様子 キャンパス(会場)は全部で6つあります。地図の 黄色 がキャンパスです。南北で4km以上離れているため、無料のシャトルバスが運行されています。 引用: https://reinvent.awsevents.com/campus/ メインのキャンパスはベネチアン(地図の3)です。その隣のミラージュ(地図の8)に私たちは宿泊していましたが、ホテルの部屋からKeynote会場までは徒歩15分ほどかかりました。それほど各ホテルが巨大です。 Googleストリートビュー。正面がベネチアン、右後ろがミラージュです。 Keynote会場。1日目のMonday Night LIVEの様子です。 撮影:児島 こちらはExpo。企業のブースがたくさん並びます。 撮影:高塚 Self-paced labs。自分のペースでAWSを学習できるコーナーです。 撮影:高塚 期間中は5つのキャンパスで朝食と昼食が提供されます。ビュッフェのほか、テイクアウトできるランチボックスも用意されていました。 撮影:高塚 撮影:佐藤 撮影:纐纈 撮影:笹沢 食後のデザートもあります。 撮影:佐藤 また、食事とは別に各所でコーヒーや軽食が配られていました。 撮影:鈴木 撮影:佐藤 こちらはAWS公式グッズのストア。 撮影:高塚 4日目の夜にはre:Inventを締めくくるパーティー「re:Play」があります。キャンパスから少し離れた特設会場で行われ、DJやバンドの演奏と、ドッジボールやアーチェリータグなどのアクティビティを楽しめます。下記は全員の集合写真。 セッション紹介 ここからは各メンバーが気になったセッションを1つずつ紹介します。 Workshop From serverful from serverless Java with AWS Lambda (SVS310) Data analysis with Amazon EKS and AWS Batch (CMP355-R) Chalk Talk Prevent unintended access with AWS IAM Access Analyzer policy validation (SEC319) Breakout Session What's new and what's next with Amazon EKS (CON205) Architecting sustainably and reducing your AWS carbon footprint (SUS205) A close look at AWS Fargate and AWS App Runner (CON406) The future of sport: How Riot Games is reinventing remote esports broadcasts (CMP311) How to save costs and optimize Microsoft workloads on AWS (ENT205) Optimizing Amazon EKS for performance and cost on AWS (CON324) Workshop From serverful from serverless Java with AWS Lambda (SVS310) 計測プラットフォーム開発本部 計測システム部の児島です。私の所属する計測システム部は、今夏アメリカに向けてフィットネスアプリ ZOZOFIT をリリースしました。その認証機構にはCognitoを採用したのですが、カスタム認証フローを利用するため、Lambdaのトリガー処理を書く必要がありました。 私たちのチームは、開発言語にScalaを採用しています。一方で、LambdaとJVMの組み合わせはコールドスタートの問題があり、相性はよくありません。かつて、Lambdaを使いたいケースでPythonを採用したこともありました。最近では、より軽量なGoなどの言語もあり、多くの開発の現場でLambdaのために開発言語を分けるという選択がとても多く見られる様になりました。しかし、私たちのチームは2つ以上の言語を維持運用するほどのチーム規模もなく、言語としてはScalaのまま、LambdaのカスタムランタイムでGraalVMを選択する意思決定をしました。 In this workshop, learn how to bring traditional Java Spring applications to AWS Lambda with minimal effort and iteratively apply optimizations to enhance your serverless Java experience. Hear about best practices, performance trade-offs, and design considerations for each step to help you make well-informed decisions when bringing enterprise Java applications to AWS Lambda. 今回のWorkshopに参加したきっかけは、このWorkshopの説明欄にあったこの一節がきっかけでした。私たちは、多くの世に出ている知見からGraalVMを採用する意思決定をしたわけですが、その他に取り得た手法の検証に十分な時間がありませんでした。そのため、改めてLambdaでJVMを採用する際の選択肢と、それらの比較、それぞれの選択に潜在するトレードオフに関する知見を得たいというモチベーションがありました。 ワークショップでは、まず初めに暫定的なAWSアカウントが与えられ、Cloud9に既に必要なコード群が事前に用意された状態で、Lambda等の必要なリソースのセットアップを済ませます。そして、Spring Bootをベースとした何も最適化されてないサンプルアプリケーションをLambdaで動かすことからはじめます。そこから、TieredCompilationの有効化、依存の軽量化などを経て、GraalVMの採用まで、様々な最適化を施していきます。その過程で、実際のコード変化を確認し、ビルドと実行を繰り返し、ベンチマークを確認していきます。 結果です。既知のものも含まれていましたが、今回のWorkshopを通してあらかじめ用意されたLambdaのパフォーマンス改善の追体験ができたことは、とても有意義な時間でした。 さて、この前日のKeynoteでは、Lambda Snapshotという更なるLambdaにJVM系言語の採用を後押しする新機能の発表がありました。今回のre:Inventの他のセッションを通しても、毎年AWSがサーバーレスへの移行をサポートする機能を強化していく実感があります。今後も、こうしたアプリケーション開発者が開発に集中しやすくなる技術のリリースに期待するとともに、引き続き注視していきたいです。 Data analysis with Amazon EKS and AWS Batch (CMP355-R) 計測システム部 SREブロックの纐纈です。 基調講演では今年新しくリリースされたサービスや機能が紹介されます。その中で、AWS BatchをEKS上で利用できるようになったという発表がありました。 まず、AWS Batchは2016年のre:Inventで発表された、バッチ処理の実行基盤のマネージドサービスです。処理すべきジョブに合わせてコンピューティングリソースの配分を自動で最適化するという特徴を持っています。今まではECSでの実行環境のみだったのですが、2022年10月から新しくEKSにも対応したとのことでした。ただ、2022年12月時点ではEKS Fargateには未対応なので注意が必要です。 今回私が参加したワークショップでは、AWS Batchを使ってAmazon EKS上でデータ分析のサンプルを動かしてみるという内容でした。2時間のワークショップの中で、前半はAWS BatchのEKS対応について概要や内部構造などの説明があり、後半はAWS Workshop Studioを使って手を動かします。EKSクラスタを立てて、PrometheusやGraphanaをデプロイし、円周率の計算をするバッチ処理でクラスタに負荷をかけながらリソースの変動を見るということもできました。 ワークショップの内容はこちらからも見ることができます。 catalog.us-east-1.prod.workshops.aws Chalk Talk Prevent unintended access with AWS IAM Access Analyzer policy validation (SEC319) SRE部 ZOZO-SREブロックの鈴木です。 普段はZOZOTOWNの裏で動いているオンプレ、AWSにあるインフラの管理をするとともに社内のAWS管理者としてセキュリティ周りの対応や各種運用をしています。 今回私が参加したSEC319は「 Chalk Talk 」という形式のセッションです。スピーカの方がスライドを発表するだけでなく、横にホワイトボードが置かれており参加者も質問したりディスカッションしたりしながら、さながら大学の講義のように進んでいきます。動画配信がなく現地に行ったメンバーのみが参加できるセッションであり、現地へ行った優越感に浸れるおすすめのセッションです。発表頂いたスライドは資料欄にリンクを掲載しています。 本セッションではIAMポリシーのライフサイクルを定義し、その過程で安全なIAMポリシーを作成、メンテナンスしていくにはどうしたらいいかを議論しました。IAMポリシー作成過程の1つに「Validation」のステップがあり、ここで正しくポリシーを検証することが安全性を高める1つの手段となることを確認しました。そのValidationのツールとして「 IAM Access Analyzer 」が紹介され、実際にどのように使うかのデモをいただきました。 IAM Access Analyzerを利用する際のCI/CD構成例の1つに「CloudFormation hooks」を利用した例があげられました。参加者にはCloudFormation hooksを使っているか、どんなところで使っているのかという質問が投げかけられ、各社の真似したくなるような事例をいくつか聞くことができました。 資料 Prevent unintended access with AWS IAM Access Analyzer policy validation awslabs/aws-cloudformation-iam-policy-validator Breakout Session What's new and what's next with Amazon EKS (CON205) 計測システム部 SREブロックの西郷です。 普段は ZOZOMAT や ZOZOGLASS 、 ZOZOFIT といった計測サービスの運用に携わっています。これらの基盤にはEKSを採用しているので、業務に活かせるものがあればと思い参加しました。 今回のセッションの中で気になったトピックとしては以下です。 EKSクラスタのKubernetesバージョンアップデートにかかる平均時間の短縮 40分以上かかっていたものが10分少々にまで短縮された VPC Lattice のサポート VPC Latticeはre:Invent期間中に発表された新機能で、VPCの1機能 L7のサービス間ネットワーク通信機能を提供するとのことで、サービスメッシュに近いものの印象 サイドカーなしで利用でき、プロトコルはHTTP、HTTPSに加えgRPCもサポートしている VPCPeeringなしでアカウントおよびVPCを跨いだ接続が可能 MarketplaceのソフトウェアとAWSによって開発されている主要なOSSのプロダクトをEKS add-onsがサポート re:Invent期間中に発表された新機能、EKS add-onsを通してEKSクラスタ内にデプロイ可能 DatadogやNewRelic、KubernetesのポリシーエンジンであるKyvernoを提供するNirmata等がComing Soonなものとして上がっていた これまでにも計測ではEKSクラスタのバージョンアップの省力化を進めてきたこともあり、Kubernetesバージョンアップ自体にかかる時間を短縮できることは大きなメリットになりそうです。 また、VPC Latticeはサイドカーなしで利用できるという点が魅力的です。現時点でサービスメッシュの利用はないものの、システムの成長に伴い検討していた背景もあるため、機会があれば検証してみたいと思っています。 ※2022/12時点ではVPC Latticeはプレビュー機能です。 Architecting sustainably and reducing your AWS carbon footprint (SUS205) SRE部 ECプラットフォームサービスSREブロックの佐藤です。普段はZOZOTOWNで使用されるマイクロサービスのインフラ構築と運用を担当しています。それ以外にも、チームをまたいだ活動としてクラウドサービスのコスト可視化・最適化にも取り組んでいます。 アダム・セリプスキーCEOのキーノート でもふれられていたようにサステナビリティは今年のre:Inventでも重要なテーマの1つでした。 本セッションは、AWSにおけるサステナビリティへの取り組みと、Amazon Prime Videoでの実例を紹介する内容になっています。コスト効率の良いシステムを考える上で、今後必須の知識になるであろうと思い、このセッションに参加しました。 セッション前半では、まずAmazonのサステナビリティに対する考えが述べられました。それから気候変動の対策に関する誓約である The Climate Pledge や、データセンターで使用される100%再生エネルギーについて、ウォーターポジティブへの取り組みなどが紹介されました。Amazonのサステナビリティに対する取り組みは、下記のサイトで確認できます。 sustainability.aboutamazon.com その中で、サステナビリティとは持続可能な未来というゴールへ向かうものではなく、永続的に繰り返す活動(セッションの中ではジャーニーと呼んでいました)であると解説していました。そのために適切なリソースを適切なタイミングで使用することが重要で、つまり効率化が中核をなす要素だと説明しており、日常的に自動化・効率化に励むSREにとって刺さる話でした。 また、 Well-Architectedフレームワーク から、 リージョンを選ぶ基準としてレイテンシやコストに加えAmazonの再生エネルギープロジェクトに近い地域、または二酸化炭素排出量の少ない地域であるか検討するプラクティス や、 よりエネルギー効率の良いインスタンスを継続的に使用するためのプラクティス の解説もありました。 後半ではAmazon Prime Videoにどのようにサステナビリティのカルチャーを取り入れ、システムの効率化を行ったのか、そのプロセスの紹介がありました。プロセスの概要は下記です。 目標を設定する 二酸化炭素の排出量削減に繋がる具体的なゴールを決める。例えばネットワーク負荷を減らすようなエンコードアルゴリズムやインフラリソースの最適化など。 この時点で、この活動に必要なリソース確保をマネージャーレベルで調整する。 指標を設定する 自分たちのサービスがどれくらい二酸化炭素を排出しているかは、 Customer Carbon Footprint Tool で確認できるが、より自分たちがコントロールしやすい代替指標(プロキシメトリクス)を定義する。例えば特定のワークロードにかかるインスタンス時間など。 反復的な改善プロセスを実行する ステップ1: 改善対象を特定する。例えば適切なインスタンスのサイジングなど。 ステップ2: 改善点を評価する。 AWS Compute Optimizer などのツールを利用する。 ステップ3: 優先順位をつけて計画を立てる。得られる成果をビジネス価値に置き換え判断材料とする。同時に、成果に関心を持つ財務や利害関係者への窓口を設置する。 ステップ4: テストし検証する。単一ホストのみで動作させるなどして小規模に始める。本番トラフィックが流れない環境を用意してスケーリングもテストする。 ステップ5: 本番環境にデプロイする。 ステップ6: 結果を測定し、成功事例を別チームに展開する。 さらに、NFLの中継を独占配信した際に行われた取り組みが取り上げられていました。それは社内から効率化の専門家を集め、各サービスのコードとスタックについてパフォーマンスレビューをするというものです。これは現在弊社で行っている正月セールに向けた取り組みと重なる部分があり、案外こういう社内の関心が高いイベントのときこそ、周りを巻き込みやすいので新しい挑戦がしやすいのではと思いました。 またスパイク対策として、重要度の低い機能をオフにするコンティンジェンシースイッチと呼ばれる仕組みの導入や、クライアントサイドでのプリキャッシュについて説明がありました。これらは持続可能な方法でスケールできるようにデザインパターン化しているとのことでした。 セッション全体を通しての気づきは、我々が普段行っているパフォーマンスチューニングやコスト最適化は、実はサステナビリティな活動そのものだということです。そして業務にはコストやビジネス要件、コンプライアンスなど優先すべきことが多いですが、まずは同じ天秤に乗せるのが大切であると感じました。そうすることで、今後の設計や技術選定などにおいて別の観点が加わり、組織や社会にとってより良い判断ができると思いました。 A close look at AWS Fargate and AWS App Runner (CON406) ブランドソリューション開発本部 バックエンド部の笹沢( @sasamuku )です。 Archana SrikantaさんよりAWS Fargateの裏側の仕組みが紹介されました。re:Invent 2019における 同氏らの発表 と重複する箇所もありましたが、今回はよりセキュリティと可用性に焦点を当てた内容となっていました。以下、抜粋してご紹介します。 前半はコンピューティングサービスの進化について説明がありました。Amazon EC2が主役だった頃と比較するとAWSの責任領域が次第に拡大しています。直近にリリースされたAWS App Runnerでは、ソースコードあるいはDockerイメージを用意して基本的な設定をするだけでスケーラブルなサービスを公開できます。 後半はセキュリティに関する興味深い話がありました。AWS Fargateはマルチテナントなサービスです。悪意あるユーザによって脆弱性を突かれれば他のユーザにも影響が出かねません。AWSは仮想的にハードウェアを分離することでこれを防止しています。具体的には、Firecrackerと呼ばれるMicro VMをタスク1つにつき1台用意しています。また、タスク間やベアメタルサーバ間の意図しない通信を許可していません。 他にもセルラーアーキテクチャと呼ばれるAWS Fargateの可用性に関する仕組みも紹介されていました。Amazon ECS on AWS FargateやAWS App Runnerを利用している方にはぜひおすすめしたいセッションの1つです。 The future of sport: How Riot Games is reinventing remote esports broadcasts (CMP311) SRE部 ZOZOSREブロックの秋田です。オンプレとクラウドのハイブリットな環境を運用保守するチームに所属しています。 私は普段からesports観戦が好きで、特にRiot Gamesが出しているLegue of LegendsやVALORANTのタイトルをよく観戦しています。大会の裏側で全世界へどのようにして配信をしているか興味がありこのセッションを聴講することにしました。 このセッションでは、Riot Gamesが従来の配信方法からどのようにeスポーツの配信方法を進化させたか、どのように配信基盤を作成しているかという話がされました。 特に興味を引いたものがRiot Directです。世界中に専用のネットワークを有しており、オンライン対戦で使われるほか、配信でもこれが使われています。 Riot Directは以下のようなものになります。 Riotのパケットに特化したケーブルとルーターを世界中に展開 Cold-potato routing 世界中で数Tbpsの帯域が利用可能 IXPやPNIを経由して世界の主要ISPにピアリング可能 独自のDDoS軽減ソリューションと既製品のDDoS軽減ソリューションの混在 また、このRiot Directですが3500以上ものBGPセッションが管理されており、様々な地域と拠点で相互接続することが可能になっています。 ZOZOTOWNのサービスとはかけ離れているセッションでしたが、普段は聞くことが出来ない世界トップゲームの話をリアルで聞けてとてもいい経験になりました。 How to save costs and optimize Microsoft workloads on AWS (ENT205) SRE部 カート決済SREブロックの伊藤です。ZOZOTOWNはWindowsサーバーやSQL Serverを使用して稼働している箇所が多くあります。それらのコスト最適化の参考になればと本セッションを受講しました。 techblog.zozo.com techblog.zozo.com 一部を抜粋してご紹介させていただきます。 FCI(Failover Cluster Instance)を構築する SQL Serverで高可用性を実現する方法としてAlwaysOnが使われがちではあるが、FCIを構築するという方法がある FCIの場合必要なSQL Serverのライセンスは1つだけであり、コストを抑えることができる aws.amazon.com EC2のCPUオプションを指定する メモリを多く使用したいが、CPUはそこまで必要ないなど一致するインスタンスが存在しない場合にvCPUの数を抑えることができる インスタンス自体のコストは変わらないが、SQL Serverなどコア単位でライセンス量が変わってくる場合にコストを抑えることができる 具体的にはr5.2xlarge(8core, 64GB)を選んでvCPUを4coreに抑えると40%の節約となる docs.aws.amazon.com その他、NitroSystemであるが故にリソースを効率良く利用できることや、既存環境を分析してレポートしてくれる OLA(Optimization and Licensing Assessment) に関しての説明などがありました。 Optimizing Amazon EKS for performance and cost on AWS (CON324) SRE部 ECプラットフォームサービスSREブロックの三品です。普段の業務でEKSを利用しているので、EKSのコスト最適化に関するこちらのセッションに参加しました。 前半はKubernetesのコスト監視の難しさと、どのようなところでコストが発生しやすいかについて解説されていました。前半の最後には、Kubernetesのコストをどのように可視化したら良いかが述べられていました。 後半では、コストを最適化するためにはどうしたら良いかについて解説されていました。具体的にはAWS Gravitonインスタンス、EC2 Spot、Karpenterについて実際の企業事例を踏まえて紹介されていました。 AWS Gravitonインスタンス AWS Gravitonインスタンスの特徴(一部抜粋) C7gインスタンスは、計算集約型のワークフローに最適な価格性能を提供しています 同クラスのEC2インスタンスと比較して60%エネルギー効率化されています (注意)AWS Gravitonインスタンスを利用するためには、ARM互換のイメージを作成する必要があります EC2 Spot EC2 Spotの特徴(一部抜粋) インスタンスタイプの種類やAZを増やすことでSpotの可用性が向上し、中断が少なくなくなります 時間に融通の効くワークロードは、空き容量を待つことができるのでEC2 Spotは最適です EC2 Spotは短期間の中断可能なワークロードに適しており、逆に長時間稼働するようなJobやstatefulなワークロードには適していません Karpenter Karpenterの特徴(一部抜粋) EC2 SpotやAWS GravitonインスタンスなどをKubernetesネイティブに統合しています ノードの起動を高速化することでコストを最小限に抑えることができます オペレーションのオーバーヘッドを削減することによって開発者はサービス開発/運用に集中できます 個人的には、前半で紹介されたkubecostに興味を持ちました。まずはkubecostを導入して適切なリソースを割り当てられているかを確認することが、コスト最適化の糸口を探すことになるはずなので、検証してみたいと思います。 おわりに re:Inventは多くのセッションをオンラインでも視聴できます。それにも関わらず、わざわざ現地参加する価値とはなんでしょうか? 会場をキャンパスと呼ぶとおり、re:Inventは学習型のカンファレンスです。しかし、その雰囲気はまるで「お祭り」です。AWSの大好きな人々が世界中から集まり、皆で学び合い、熱く語らう「お祭り」です。この盛り上がりは現地でしか感じられません。 また、WorkshopやChalk Talkなどの体験型セッションが豊富で、そこに集まる参加者のレベルも高く、濃密な知見を得られます。AWSやパートナー企業のエンジニアと直接話せる貴重な機会でもあります。 もちろん、オンライン参加でも多くの学びを得られると思います。KeynoteやBreakout Sessionは1つ1つのクオリティがとても高く、たくさんの新情報を含んでいます。 でも、この刺激、特に 他のエンジニアから受ける刺激 は、ラスベガス現地でしか味わえないと確信します。この刺激を求め、私たちは来年もre:Inventに参加するはずです。 今回得た知見と刺激を社内外に共有しながら、これからもAWSを活用してプロダクトとビジネスの成長を支えていきます! ZOZOではAWSが大好きなエンジニアを募集しています。奮ってご応募ください。 corp.zozo.com corp.zozo.com 最後に、ラスベガスでお世話になった方々へ御礼申し上げます。ありがとうございました。
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの渡辺です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。 先日私達のチームでは、リリースフローにステージング環境での負荷試験を自動化する取り組みを行いました。今回説明する「負荷試験の自動化」が何を表すのかを定義すると、ここではステージング環境のアプリケーションバージョンを変更した際に、人の手を介さずに負荷試験が行われることを指します。 Kubernetes環境における負荷試験の自動化を検討している方は是非参考にしてください。 目次 はじめに 目次 負荷試験を自動化する前の課題 負荷試験のシナリオ設計 目的設定 現状調査 目標値設定 シナリオ設計 負荷試験を自動化する取り組み 構成 処理の流れ シナリオに沿ったリクエストを送る 実行結果をS3に保存してSlackに通知する 負荷試験で作成したデータを削除する 負荷試験の実行トリガー 今後の改善ポイント 自動化の精度向上 リリース判断の自動化 まとめ 終わりに 負荷試験を自動化する前の課題 私達のチームではEKS環境でアプリケーションを管理しており、これまでArgo CDやArgo Rolloutsなどのツールを導入することで、リリース安定性を向上させてきました。 techblog.zozo.com techblog.zozo.com 特にArgo Rolloutsを導入したカナリアリリースにより、アプリケーションエラーやレイテンシ悪化などによるユーザへの影響を最小限にできました。 しかしながら、ステージング環境での動作確認だけでは見落とされる問題については、カナリアリリース後にアラート通知がくるなど未然に防げませんでした。例えば、ライブラリ更新によるメモリリークの発生がリリース後に発覚する事象などが挙げられます。 負荷試験のシナリオ設計 そこで、上記の問題を解決すべく、ステージング環境で負荷試験を自動化する取り組みを行いました。 まずは、負荷試験のシナリオを設計するために行ったことを紹介します。 目的設定 負荷試験はあくまで手段であるため、目的を見失わないようにあらかじめ「アプリケーションの特性」「現在の課題」「負荷試験の目的」などをドキュメント化しました。 負荷試験と一口に言っても、目的によってシナリオが変わってくるので、多少面倒でも目的をまとめておくことをオススメします。 自分達が負荷試験で何を実現したいのかを検討した結果、「負荷試験を自動化する前の課題」でも説明しているようにリリース後の性能悪化を事前に検知することが一番の目的となりました。 ピーク時の性能テスト ロングランテスト テスト目的 在庫数よりも需要が大きく上回る商品の発売やメディア露出によるリクエストピーク時に十分なパフォーマンスが得られるか確認する 通常時に十分なパフォーマンスが得られるか確認する(メモリリークの発生など長時間テストしないと表面化しない問題の確認) 「ピーク時の性能テスト」を基本として、「ロングランテスト」はオプションとして設定しています。メモリリークなどロングランテストを行うことで検知の精度が高まるものもありますが、リリース作業に数時間の負荷試験を実行することは現実的でないと判断しました。 なお、今回は見送りましたが、今後は以下の観点でも負荷試験を活用したいと考えています。 システム限界点を見極める 予想外のスパイク発生を想定したテストを行い、短時間に処理が集中した場合に発生する現象を確認する 限界を超えた場合にどのような現象が発生するか確認する 現状調査 テスト目的から、ピーク時の本番トラフィックを基にシナリオを作成するため、各APIごとリクエストの実績を調査しました。時間帯や曜日ごとに数が変化しているため、現状調査を通じてサービスの理解やユーザ行動などを深く認識できました。 負荷試験とは直接関係ありませんが、サービスのドメイン知識が少ないエンジニアにとって、現状調査をすることで得られるものは大きいと実感しました。このため、入社間もないドメイン知識が少ない方こそ負荷試験のタスクに取り組むことをオススメします。 目標値設定 負荷試験の実行結果と比較するための目標値を設定しました。 「目標値をクリアできないとリリースしない」などの指標にするため、既にSLOやSLAを策定している場合はその数値を利用してもいいと思います。しかし、私たちのチームでは各APIごとのSLOを策定していないため、導入初期の段階として「現状のレイテンシやエラー率を上回らない」ことを目標とする値を設定しました。 今後、各APIごとのSLOを策定した際には、こちらの目標値も更新していくつもりです。サービスの成長と共に目標値は変わってくるため、一度設定して終わりではなく定期的に見直すことが大切です。 シナリオ設計 上記の調査で得た情報を基にシナリオを設計します。 計測プロダクトを利用するユーザの行動は、大きく2つに分けられます。ユーザ行動はとてもシンプルなので、シナリオもシンプルになります。 計測ユーザ(計測行動) 商品閲覧ユーザ(計測後の行動) 例えばZOZOGLASSの計測ユーザ行動は以下のようになります。 2パターンのユーザ行動のシナリオを設計し、目的設定や現状調査で積算したパラメータ(テスト時間やリクエスト毎秒など)を割り当てていきます。計測ユーザよりも商品閲覧ユーザの方が多いため、以下のようなイメージでシナリオを実行していきます。 なお、先ほどオプションとしてロングランテストを設定していると説明しましたが、パラメータを環境変数で設定することによりテスト目的に合わせて簡単に切り替えられるようにしています。 負荷試験を自動化する取り組み それでは、負荷試験を自動化する取り組みについて紹介します。 私たちはKubernetesマニフェストをGitHubで管理しており、Argo CDによるGitOpsを実現しています。 Podのコンテナイメージのタグ更新PRをmainブランチにマージした時、Argo CDによりステージング環境のPodが入れ替わります。 リリース担当者はPRのマージを行うまでを担当し、Podの入れ替えから負荷試験の実行までを自動的に行うような仕組みを構築しました。 構成 負荷試験ツールとしては、社内でも利用実績のあるGatlingを採用しました。 GatlingはテストシナリオをScalaで記述でき、結果レポートをHTMLで自動生成してくれます。計測プロダクトではバックエンドにScalaを採用していることもあり、馴染みのある言語でシナリオを作成できました。 処理の流れ 負荷試験を実行するPodが行うことは以下になります。 シナリオに沿ったリクエストを送る 実行結果をS3に保存してSlackに通知する 負荷試験で作成したデータを削除する シナリオに沿ったリクエストを送る 負荷試験を実行するコンテナをアプリケーションのサイドカーに設置してリクエストを流すこともできます。 しかし、計測サービスではレイテンシを重要視しているため、より本番トラフィックに近いネットワークでリクエストを送信したいという要求がありました。そこで、負荷試験を実行するPodからNAT Gatewayを経由する設計にしました。 ステージング環境は、ALBのルールで限られたIPからのリクエストのみ受け付ける設定をしています。NAT Gatewayに紐付けたElastic IPを許可することでリクエストを受け付けるようにしました。 実行結果をS3に保存してSlackに通知する リリース担当者が負荷試験を待っている間に別の作業をしていると、つい負荷試験の完了に気づくのが遅れてしまいます。結果的にリリース作業も遅れるため、負荷試験の完了時にSlackへ結果レポートのリンク先(S3)を通知します。リンクをクリックするだけで結果を確認できるので、リリース担当者の手間が省けます。 負荷試験で作成したデータを削除する 負荷試験ではダミーデータで計測するため、ダミーデータがDBやS3に蓄積されていきます。塵も積もれば山となるため、負荷試験の実行後に該当データを削除しコスト負担が大きくならないようにしています。 負荷試験の実行トリガー 私達のアプリケーションはJVM環境で動作しており、アプリケーションコンテナがデプロイされる際は、リクエストを受け付ける前に暖機運転を行っています。 techblog.zozo.com また、起動中のPod数をコントロールするためのPodDisruptionBudget設定やカナリアリリースを採用していることもあり、すべてのPodが入れ替わるまでにタイムラグが生じます。 このため、アプリケーションのデプロイと同じトリガーで負荷試験用のPodを起動すると、全てのPodが新しいバージョンへ入れ替わる前に実行される場合があります。 これでは、正しい試験結果を得ることはできません。負荷試験の開始をsleepなどで調整することも考えられますが、確実にPodが入れ替わった保証にはなりません。 そこで、全てのPodが新バージョンに入れ替わったことを負荷試験のトリガーにするため、当初はKubernetesのカスタムリソースを作成することを検討しました。 しかし、コントローラのメンテナンスなどを考慮すると負担も大きく感じたので、他の方法がないかネットの海を探索していたところ、Argo CDの Resource Hook が目に留まりました。 Jobリソースのannotationsに argocd.argoproj.io/hook: PostSync を設定するだけでデプロイ後のヘルスチェックが成功した後でJobを起動できます。たった一行追加するだけで要件を満たすことができたので、Argo CDを導入した恩恵を受けました。 今後の改善ポイント 自動化の精度向上 Argo CDのResource Hookはとても便利なのですが、Pod関連以外のリソース変更時もトリガーになるところがコントロールできません。 このため、Kubernetesリソースの変更があった場合(例えばServiceAccountを作成するなど)、アプリケーションは変更していないのですが負荷試験が実行されてしまいます。 現状、Kubernetesリソースの変更や追加は頻繁に発生していないので、意味のない負荷試験が実行される回数は少ないです。しかし、開発が盛んなプロダクトへ導入する際は、この辺を工夫する必要がありそうです。 リリース判断の自動化 今回、負荷試験の自動化は実現できましたが、リリースの判断の自動化までは実現できていません。 現状は、Slackに通知された結果レポートと目標値設定で策定した値を比べて、リリース担当者がリリースの可否を判断しています。 ヒューマンエラーを防ぐことはできていないため、レイテンシの悪化に気づかずリリース作業を進める可能性もゼロではありません。 また、負荷試験の結果を待たずとも本番リリースはできてしまうので、今後は負荷試験の結果でリリース判断を機械的に判断する仕組みを構築していきたいです。 まとめ 今回は計測システムのリリース業務の安定性向上の一例として、負荷試験を導入する方法についてご紹介しました。ステージング環境のデプロイ時に自動で負荷試験を実行する仕組みを構築したことで、より安全に定期リリースを行えるようになりました。 負荷試験は導入して終わりではないので、今後はさらなるサービスの安定性に役立てるため積極的に活用していきたいと思います。 終わりに 計測プラットフォーム開発本部では、ZOZOMATやZOZOGLASS、ZOZOFITなど様々なサービスを展開しています。今後も新しいサービスや機能を開発する予定であるため、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの近藤です。普段はZOZOMATやZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。 今年の夏に、ZOZOFITというサービスがローンチされました。このサービスは米国での展開を行い、日本ではあまり目にすることのないサービス名称だと思います。 ZOZOFITをローンチするにあたり、私達のチームではアーキテクチャを設計し、システム構築をすることになりました。 本記事では、ZOZOFITの開発時に遭遇した課題と対応方法について紹介します。 目次 はじめに 目次 ZOZOFITについて システム構成 レイテンシを考慮したAWSのリージョンの選定 認証機構の構築 開発中の課題 米国でSMSの送信時に必要な申請 SMSの送信検証 振り返り 終わりに ZOZOFITについて ZOZOFITは、ZOZOSUITの計測技術を利用したサービスです。2022/11時点では、体型計測および身体3Dモデルのデータ・体脂肪率の表示機能を提供しています。 ZOZOFITの大きな特徴は、ZOZOMATやZOZOGLASSと異なり、ZOZOTOWNから完全に独立したサービスであることです。 システム構成 ここでは、ZOZOFITのシステム構成を紹介します。以下にネイティブアプリから呼び出される部分にフォーカスした、システム構成図を記載します。 図1 : ZOZOFIT システム構成図 ZOZOFITは既存の計測系サービスと同じく、AWSのEKSを採用しています。また、その他技術スタック部分も既存サービスと揃えていますが、米国向けかつZOZOTOWNから独立したサービスというZOZOFIT独自の特徴を持っています。 この特徴により、システム構成も既存とは異なる部分が生まれました。次節以降で、これらの異なる部分が生まれた背景について説明します。 既存の計測系サービスのシステム構成については、ZOZOMATのシステム構成のブログ記事が公開されているので、気になる方はこちらの記事をご参照ください。 techblog.zozo.com レイテンシを考慮したAWSのリージョンの選定 計測プラットフォームでは、シングルクラスタ・マルチテナントの構成を前提としたEKSを運用しています。こちらの運用方針については、チームメンバーのブログ記事も公開されているので、気になる方はこちらの記事をご参照ください。 techblog.zozo.com ZOZOFITは米国向けのサービスですが、別のリージョンにクラスタを構築すると運用方針からは外れることになります。このため当初はGlobalAcceleratorを利用し、日本国内のリージョンにシステムを構築することも検討していました。 日本国内にシステムを構築するにあたり、性能評価の前段階として、データの越境移転について法務に確認を取りました。 結論として、米国内で最も厳しいカリフォルニア州の個人情報保護法と照らし合わせ、サービス構築時点では問題ないことがわかりました。 法律的な面での懸念事項がクリアできたので、次はシステム面での懸念事項の検証に入りました。 日本国内に構築した場合における最大の懸念はレイテンシです。物理的な距離が遠くなれば、レイテンシに影響が出るのは避けられない問題です。 私達は実際にどの程度の影響が出るか、レイテンシに関して日本国内のリージョンと米国内のリージョンで比較検証を行いました。比較検証する米国内のリージョンは、ビジネスサイド側から顧客のターゲット層をヒアリングし、オレゴン(us-west-2)を利用することになりました。 速度検証には、AWS Global Accelerator 速度比較ツールを利用しました。 docs.aws.amazon.com speedtest.globalaccelerator.aws 通信サイズ ap-northeast-1(東京) us-west-2(オレゴン) 50KB 100KB 1M 2M 図2 : LosAngelsからの速度検証の結果 検証の結果、GlobalAcceleratorによる改善効果は大きいものの、日本国内のリージョンを利用した場合、米国内のリージョンを利用した場合と比較すると、おおよそ1.5倍のレイテンシとなることがわかりました。この結果を受け、サービスにおけるレイテンシへの影響を考慮し、米国のリージョンにシステムを構築することになりました。比較検証を通して、通信における物理的な距離はレイテンシに大きな影響を与えることが改めてわかりました。また、オーバヘッドの具体的な数値を出すことでリージョンをスムーズに決められました。 認証機構の構築 ZOZOFITは、ZOZOTOWNから独立したサービスのため、ユーザーの認証機構を新規に構築する必要がありました。今回の認証機構ではシステム要件として多要素認証が求められており、私達はCognitoとAuth0のどちらを採用するかについて検討しました。 どちらも認証機構としての要件を満たしますが、ZOZOFITの開発は速度を求められていたこともあり、AWS内で完結でき導入・検証が即時にできるCognitoを採用することにしました。 開発中の課題 上述したように、ZOZOFITは米国のリージョンにシステムを構築することになりました。ここでは開発中に遭遇した課題の中でも、普段利用しない日本以外のリージョンを利用したことに起因した課題と対応方法について紹介します。 米国でSMSの送信時に必要な申請 認証機構の構築を進める中で、米国でCognitoからSMSを送信するには送信元IDの取得が必要であるとわかりました。送信元IDは、SMSの送信者が誰であるかを示すIDであり、国によって送信元IDとなり得るものが異なります。 今回は、米国で送信元IDとなりえる3種類(ShortCode、10DLC、TollFreeNumber)の中から、選択することになりました。なお、AWSでは、Pinpointというサービスで送信元IDを取得できます。 特徴 ShortCode TollFreeNumber 10DLC フォーマット 5-6桁 10桁 10桁 事前審査 必要 不要 必要 発行にかかる予想日数(審査にかかる日数は含まない) 12週間 即時 1週間 スループット 100/秒(有料で上限を上げることが可能) 3/秒 最大100/秒(後述する審査結果により変動) 必須キーワード Opt-in, opt-out, and HELP STOP, UNSTOP(オプトアウトおよびオプトインメッセージの変更不可) Opt-in, opt-out, and HELP 図3 : 各送信元IDの特徴 docs.aws.amazon.com ZOZOFITの送信元IDの要件は、簡単にまとめると以下の3点でした。 2週間以内に利用を開始したい サービスの初期フェーズのため、秒間の最大リクエスト数は30を想定 サインアップ・サインインに利用する経路で、キャリア都合でのブロックは回避したい 各送信元IDを比較し、要件にマッチした10DLCを利用することにしました。以下で、AWSのPinpointで10DLCを送信元IDとして利用する流れを説明します。 企業情報の登録 企業情報の審査 キャンペーンの作成 10DLCの番号取得 正確には2を行わなずとも10DLCの番号は取得できますが、送信数に制約がかかります。このため、商用環境での利用に向けて事前にこれらの申請作業を行いました。 具体的に、送信数に関する制約は以下の図の通りです。 審査を受けなかった場合は75msg/min、2000/dayの上限となり、ZOZOFITの商用環境で利用するには厳しい数値でした。 図4 : 10DLCにおける送信数の制約 (図は AWS公式ドキュメント からの引用) なお、開発用途では、送信元IDとしてTollFreeNumberを利用する方針としています。 これは開発用途では送信数が限定的であることに加え、10DLCの取得時に登録する企業情報が同一だった場合、複数アカウントでQuotaが共有されるためです。 計測プラットフォームでは環境単位でAWSアカウントを分離しています。このため、複数の環境で送信元IDとして10DLCを利用した場合、本番環境に他環境での作業が影響してしまう危険があります。この問題を回避する為、本番と検証の設定に差分が生まれることを許容しました。 SMSの送信検証 認証機構の構築が進む中で実際にSMSの送信検証を行う際に、開発者の手元にある携帯電話を利用したところ、SMSが届かない問題に遭遇しました。調査を進める中で、SMSの配信失敗のエラーログを有効化したところ、以下のエラーメッセージがログに出力されていることがわかりました。 { " numberOfMessageParts ": 1 , " destination ": " xxxxxxxxxxxxx ", " priceInUSD ": 0.00831 , " smsType ": " Transactional ", " providerResponse ": " Phone carrier is currently unreachable/unavailable ", " dwellTimeMs ": 72 , " dwellTimeMsUntilDeviceAck ": 1526 } エラーメッセージから、送信先のキャリアにSMSがそもそも届かないのではという懸念が生まれました。この問題に対して、米国の電話番号で検証し、原因が送信側か受信側かを切り分けをしようと考えました。この調査の為、米国の電話番号を取得することになりました。 取得する電話番号は、SMSの受信ができ、開発者が容易にメッセージを確認できる必要がありました。 これらの要件を満たす、 Twilio とPinpointの2つから選択することにしました。 結論としては、新規でアカウントの発行管理が不要、かつ、コード管理がしやすいPinpointを利用することにしました。 図5 : SMSのメッセージの送信経路 取得した電話番号で検証した際、各所でのログ出力は以下の結果となりました。 調査対象 日本の携帯電話 米国の携帯電話 Pinpointで取得した電話番号 httpサーバログ ○ ○ ○ アプリケーションサーバログ ○ ○ ○ CloudTrail(CognitoのAPIの呼び出し履歴) ○ ○ ○ SMSの配信エラーログ ○ ✖️ ✖️ SMSの配信ログ ✖️ ✖️ ✖️ 調査の結果は、米国の電話番号であってもSMSが届かない状態でした。この結果から、Cognito側の設定もしくはアプリケーションの実装の問題である可能性が高いという判断になりました。 最終的には、Cognito側の設定である auto verifyの設定 が電話番号に対して有効になっておらず、SMSの送信が行われていないことが原因でした。日本の携帯電話の番号を登録した場合はエラーメッセージが出力されていたので、SMSの送信そのものは行われていると判断していました。調査の過程で、このエラーメッセージはSMSの送信段階で出力されているのではなく、CognitoのUserPoolのデータが更新された際に行われる内部的な処理の中で出力されていることがわかりました。 なお、このトラブルを解決する過程でPinpointによって取得した番号をSNSTopicに紐づけられることがわかりました。これは検証用途でSMSのメッセージを取得する方法として活用できました。 図6 : SMSメッセージのメールへの転送経路 振り返り 今回、ZOZOFITの開発では既存サービスと異なる部分があり、チームとして新しい知見獲得の機会を得られました。反面、知見のない問題に遭遇しましたが、チーム全体で前向きに対応を進めることで解決出来ました。 リージョンの選定方法やSMS送信時の制約、ユーザの認証基盤をゼロから構築したことも、開発チームにとって大きな経験となりました。 最後に付け加えると、ZOZOFITは2022年の夏にローンチするという大きな目標がありました。この大きな目標に向け、チームとして知見の低い課題に対する対応が様々な場所で求められましたが、地道に検証を行い、結果を調査することで着実に前に進められました。この結果、大きなトラブルなくZOZOFITをローンチできました。 終わりに 計測プラットフォーム開発本部では、今回紹介させていただいた ZOZOFIT のように、日本国内に限らず新しいサービスを開発していく予定です。 今回、ZOZOFITの開発チーム構成には触れませんでしたが、 ZOZO New Zealand 、 ZOZO Apparel USA 、ZOZOの3つの組織での共同開発となっています。技術力以外に英語によるコミュニケーション能力も求められますが、このような環境を楽しみ、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
はじめに ZOZOMO部プロダクト開発ブロックの木目沢です。 11/25にZOZOとChatworkさん合同でCQRSをテーマにした CQRS Meetup【Chatwork × ZOZO】 を開催しました。 zozotech-inc.connpass.com 開催の経緯 ZOZOMO部プロダクト開発ブロックでは、CQRSアーキテクチャの採用に際し、ZOZOの技術顧問であるかとじゅんさん(加藤潤一さん/ @j5ik2o )にアドバイスをいただいてきました。今回採用したCQRSアーキテクチャをもっと広く知っていただこうと、かとじゅんさん所属のChatwork社と共同でイベントを開催しました。 登壇内容まとめ 本イベントでは、ZOZOエンジニアの岡元と、かとじゅんさんに登壇いただきました。さらに、CQRSに対して一家言ある両者によるパネルディスカッションを行いました。 当日の様子はYouTubeのアーカイブをご覧ください。 両者の発表資料はSpeaker Deckに公開されています。 店舗在庫連携のCQRSを支えるメッセージング周りの技術(株式会社ZOZOブランドソリューション開発本部ZOZOMO部/岡元政大) 弊チーム岡元の登壇では、 ZOZOTOWN上での「ブランド実店舗の在庫確認・在庫取り置き」機能 で採用されているCQRSアーキテクチャの詳細を共有しました。 OutBoxパターンとCDCパターンを組み合わせたアーキテクチャになっており、どのような経緯でこのアーキテクチャを採用したか、どんな利点があるかなどを説明しました。 合わせてテックブログ DynamoDBによるOutboxパターンとCDCを用いたCQRSアーキテクチャの実装〜ZOZOMOでの取り組み もぜひ御覧ください。 techblog.zozo.com AWSデータベースブログの記事「Amazon DynamoDBによるCQRSイベントストアの構築」を勝手に読み解く(Chatwork株式会社/加藤潤一) かとじゅんさんの登壇では、AWSデータベースブログの 「Amazon DynamoDBを使ったCQRSイベントストアの構築」 という記事を紹介いただきました。元のブログ記事のCQRSのアーキテクチャの説明を補足する形で詳細に解説いただきました。 パネルディスカッション パネルディスカッションではお互いの登壇の感想戦、及び視聴者の皆様の質問にお答えする形で話していただきました。 今回の発表のどちらを選択するかの判断基準として、コマンド側のDynamoDBのトランザクションを許容できるかが論点となっていました。お二人の結論としては、トランザクションを許容できる場合は岡元の登壇内容のような実装の方法、できない場合はかとじゅんさんに解説いただいたアーキテクチャで実装できそうだということでした。 どちらもCQRSアーキテクチャを実装するための特別なソフトウェアを利用することなく実現できます。そして、AWSマネージドなサービスのみを利用して実現できることも紹介できました。敷居が高いと思われているCQRSアーキテクチャが広がるきっかけを作れたのではないでしょうか。 最後に ZOZOでは、プロダクト開発以外にも今回のようなイベントの開催など外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
こんにちは、最近気になるニュースはサザエの学名が数年前に初めて命名されたこと 1 な、MLデータ部データ基盤ブロックの塩崎です。BigQueryのストレージに関する新料金プランが先日発表されたので、その検証をしました。我々の環境では年間で数千万円という費用削減を達成できることが分かりましたので、BigQueryに多くのデータを蓄積している会社は是非お試しください。 ストレージ費用の悩み データ基盤を長期間運用していると、データ量の増加が問題になることがしばしばあります。特にユーザーの行動ログやスタースキーマにおけるファクト系テーブルなどはデータがどんどん蓄積されます。古いデータを削除することでデータ量の増加を緩和できますが、それでもサービスの成長に伴いデータ量は増加する傾向になります。 BigQueryはコンピューティングとストレージが高度に分離されているので、初期のAmazon Redshiftのようなストレージを増やすためにCPUも増やす必要はありません。ストレージのみを独立して拡張でき、柔軟なキャパシティプランニングを行えます。これによってある程度は悩みが軽減されましたが、データ量が増えるに従って増加するストレージコストは依然としてデータ基盤運用者の悩みの種になり続けています。先日発表されたBigQueryストレージに関する新料金プランに切り替ることで、データを削減せずにコストを削減できる可能性がでたので、検証しました。 従来の料金プランの紹介 まずは、従来からある料金プランを解説します。これ以降の解説はUS Multi Regionを対象にして行いますので、Tokyo Regionなどの他リージョンでBigQueryを利用されている方は一部の数字が異なります。ご注意ください。 従来の料金プランではlogical storageという「非圧縮」状態のデータ量に応じた費用が発生します。この費用は1GBのデータを1か月保管する毎に0.02USDもしくは0.01USDです。データを格納した直後はActive logical storageというタイプで課金され単価が0.02USDです。また、90日間テーブル・パーティションに対する変更処理を行わなかった場合は、Long-term logical storageに自動的に移行されます。Long-term Storageは単価がActive Storageの半額である0.01USDになります。 cloud.google.com データ量はデータ型毎に決まり、例えばINT64型ならば8byte、STRING型ならば2byte + UTF-8でエンコードした時のbyte数になります。 cloud.google.com BigQueryのWeb UIではテーブルを選択した後にDETAILSタブをクリックした後の画面で確認ができます。例えば以下のテーブルでは、Active logical bytesは6.23TB、Long term logical bytesは958.09GBであることが分かります。 それぞれに単価をかけ合わせることで、従来のプランでの費用が分かります。 5.3 * 1024 * 0.02 + 958.09 * 0.01 = 118 USD 新料金プランの紹介 次に新料金プランを解説します。 新料金プランではphysical storageという「圧縮済」状態のデータ量に応じた費用が発生します。この費用は1GBのデータを1か月保管する毎に0.04USDもしくは0.02USDです。データを格納した直後はActive physical storageというタイプで課金され単価が0.04USDです。また、90日間変更をしなかった場合はLong-term physical storageへ移行され0.02USDになります。1GBあたりの単価が従来のものと比べて倍になっていることが分かります。 単価の上昇効果と圧縮によるデータ量の削減効果の両方が作用するので、圧縮によってどの程度データ量が削減されるか次第でどちらが安いのかが変わります。例えば、圧縮でデータ量が1/2になる場合は、データ量削減効果と単価の上昇が打ち消し合い費用は等しくなります。また、圧縮でデータ量が1/10になる場合は圧縮による効果の方が大きいので、新料金プランの方が安価になります。 physical storageに関する情報もlogical storageと同じ画面で確認できます。例えば先程と同じテーブルでは、Active physical bytesは503.19GB、Long term physical bytesは59.89GBであることが分かります。 それぞれに単価をかけ合わせると、新プランでの費用がわかります。 503.19 * 0.04 + 59.89 * 0.02 = 21.3 USD 従来のプランでの費用は118USDでしたので、このテーブルは新プランの方が安価です。 また、新プランの注意点としては、タイムトラベル用のデータに対する費用の発生が挙げられます。クエリを実行するときにテーブル名の後ろに FOR SYSTEM_TIME AS OF <特定の日時> という指定をすると、7日以内であれば任意時点のテーブル情報を取得できます。 cloud.google.com この機能を実現するためにBigQueryは更新・削除されたデータをTime travel Storageに移動されています。従来のプランはこのTime travel Storageに対する費用がかかりませんが、新プランではActive Storageと同じ1GBあたり0.04USDかかります。そのため、更新・削除を頻繁に行うテーブルはその分の費用がプラスされる点に注意が必要です。なお、BigQueryのWeb UIから確認できるActive physical bytesにはこのTime travel Storageの容量も含まれています。 Active physical bytes - Time travel physical byte という計算で、純粋なActive physical Storageの容量を計算できます。 新旧の比較 では、ここで一旦新旧プランの比較を以下の表にまとめます。 従来のプラン 新プラン Active Storage費用(USD / GB month) 0.02 0.04 Long term Storage費用(USD / GB month) 0.01 0.02 データの圧縮 無し 有り Time travel Storageに対する費用 無し 有り どちらが安くなるのかは一概に決められず、保存されているデータの性質に依存します。圧縮率が高く、更新削除を頻繁に行わないデータは新プランに移行した方が安くなる傾向にあることが分かります。 データセット毎の費用比較 ここからはプラン変更でどの程度安くなる・高くなるのかをデータセット毎に集計していきます。 以下のクエリを実行するとデータセット毎に saving_cost が計算されます。この値がプラスの場合は新プランに切り替えたほうが安く、マイナスの場合は従来のプランの方が安いです。クエリを実行するためには、Organizationレベルでの roles/bigquery.metadataViewer ロールが必要です。 with existing_tables as ( select table_catalog as project_id, table_schema as dataset_name, table_name from ( # 対象のGCPプロジェクトを列挙 select * from `プロジェクトID1`.`region-us`.INFORMATION_SCHEMA.TABLES union all select * from `プロジェクトID2`.`region-us`.INFORMATION_SCHEMA.TABLES union all ... select * from `プロジェクトIDn`.`region-us`.INFORMATION_SCHEMA.TABLES ) ), existing_table_storage_by_organization as ( select ts.* from `region-us`.INFORMATION_SCHEMA.TABLE_STORAGE_BY_ORGANIZATION as ts join existing_tables as et # TABLE_STORAGE_BY_ORGANIZATIONは削除済みテーブルの情報も返すのでTABLESとinner joinする on ts.project_id = et.project_id and ts.table_schema = et.dataset_name and ts.table_name = et.table_name ), all_schamata_options as ( # 対象のGCPプロジェクトを列挙 select * from `プロジェクトID1`.`region-us`.INFORMATION_SCHEMA.SCHEMATA_OPTIONS union all select * from `プロジェクトID2`.`region-us`.INFORMATION_SCHEMA.SCHEMATA_OPTIONS union all ... select * from `プロジェクトIDn`.`region-us`.INFORMATION_SCHEMA.SCHEMATA_OPTIONS ), datasets as ( select distinct catalog_name as project_id, schema_name as dataset_name from all_schamata_options ), storage_options_sub as ( select catalog_name as project_id, schema_name as dataset_name, option_value as storage_billing_model, from all_schamata_options where option_name = ' storage_billing_model ' ), storage_options as ( select d.*, # SCHEMATA_OPTIONSはEntity Attribute Valueしているテーブルで、storage_billing_modelが存在しない場合もあるため ifnull(so.storage_billing_model, ' LOGICAL ' ) as storage_billing_model from datasets as d left join storage_options_sub as so on d.project_id = so.project_id AND d.dataset_name = so.dataset_name ), cost_by_dataset as ( select project_id, dataset_name, # US以外のリージョンを対象にする場合は修正 active_logical_bytes / pow( 2 , 30 ) * 0 . 02 + long_term_logical_bytes / pow( 2 , 30 ) * 0 . 01 as logical_cost, active_physical_bytes / pow( 2 , 30 ) * 0 . 04 + long_term_physical_bytes / pow( 2 , 30 ) * 0 . 02 as physical_cost, time_travel_physical_bytes / pow( 2 , 30 ) * 0 . 04 as time_travel_cost, from ( select project_id, table_schema as dataset_name, sum (active_logical_bytes) as active_logical_bytes, sum (long_term_logical_bytes) as long_term_logical_bytes, sum (active_physical_bytes) as active_physical_bytes, sum (long_term_physical_bytes) as long_term_physical_bytes, sum (time_travel_physical_bytes) as time_travel_physical_bytes, from existing_table_storage_by_organization group by project_id, table_schema ) where long_term_logical_bytes + long_term_logical_bytes + long_term_physical_bytes + long_term_physical_bytes > 1 order by logical_cost desc ) select c.*, c.logical_cost - c.physical_cost as saving_cost, s.storage_billing_model, from cost_by_dataset as c left join storage_options as s on c.project_id = s.project_id and c.dataset_name = s.dataset_name このクエリをZOZOのデータ基盤で実行したところ、ほとんど全てのデータセットでPhysical Storageの方が安いという結果が得られました。また、Logical Storageの方が安いという結果になったデータセットを更に調査したところ、その要因はTime Travel Storageの費用によるものであることも分かりました。純粋なテーブルデータだけの費用ではPhysical Storageの方が安価でした。そのため、後述するTime Travel Windowを調整することで費用削減が可能であることを示唆しています。 Physical Storage課金への切り替え Physical Storageへの切り替え方法を説明します。 どちらの料金プランを使用するのかはデータセット単位で指定でき、以下のbqコマンドでPhysical Storageによる課金に切り替えることができます。ただし、この作業は一方通行であり、一旦Physical Storageに切り替えたデータセットをLogical Storageに戻すことが不可能な点に注意が必要です。また、Physical Storageによる課金プランは2022年10月27日時点では、まだPreview段階です。そのため、本番環境のデータに対してこのコマンドを実行することには慎重になる必要もあります。 2024-01-02 追記 Phycical Storage機能は2023年7月5日に一般公開(GA)段階に移行しました。また、Physical StorageからLogical Storageに戻すこともできるようになりました。ただし、一旦変更すると14日間は元に戻せないので、依然として切り替えには慎重になる必要があります。 追記ここまで bq update -d --storage_billing_model = PHYSICAL < プロジェクトID > : < データセット名 > cloud.google.com Time Travel Windowの調整による費用削減 最後にさらなる費用削減のためにTime Travel Windowを調整する方法を紹介します。 Physical StorageにはTime Travel用のデータも含まれているため、このデータ量を減らすことで費用削減できます。Time Travelで遡ることのできる期間(Time Travel Window)を短くすると、それに伴ってデータ量も減ります。 以下のbqコマンドでTime Travel Windowをデフォルト値である168時間(7日)より短くできます。ただし、Time Travel Windowを短くすると、もしものときにデータ復旧が不可能になるリスクを抱えることになります。費用のみを気にして無闇に小さな値にすることは避けましょう。 bq update -d --max_time_travel_hours=<遡ることのできる時間> <プロジェクトID>:<データセット名> また、Logical Storage課金のデータセットはTime Travel Storageに対する費用はゼロなので、デフォルト値から変更するメリットはないです。 cloud.google.com まとめ BigQueryの新料金プランであるPhysical Storageについて解説をし、切り替えることで費用を削減できるかどうかを確認する方法を紹介しました。データの特性にもよりますが、大きく費用を削減できる可能性のあるプランですので、データ基盤に多くのデータを蓄積している企業は試す価値があるかと思います。 ZOZOでは、一緒に楽しく働く仲間を募集中です。ご興味のある方は下記採用ページをご覧ください! corp.zozo.com 驚愕の新種! その名は「サザエ」 〜 250年にわたる壮大な伝言ゲーム 〜 ↩
こんにちは。計測システム部SREブロックの西郷です。 10月24日から10月28日にかけてKubeCon + CloudNativeCon North America 2022(以下、KubeCon)が行われました。今回弊社からはWEARやZOZOTOWNのマイクロサービス基盤、計測システムに関わるメンバー7名で参加しました。 本記事では現地の様子や弊社エンジニアが気になったセッションについてレポートしていきます。 目次 目次 3年ぶりにアメリカでの現地開催となったKubeCon現況 参加メンバーによるセッション紹介 Istio Today and Tomorrow: Sidecars and Beyond Cloud Governance With Infrastructure As Code (IaC) With Kyverno And Crossplane - Dolis Sharma, Nirmata “Why Can’t Kubernetes Devs Just Add This New Feature? Seems So Easy!” - Understanding the Feature Lifecycle In Kubernetes - Ricardo Katz, VMware & Carlos Panato, Chainguard Security In the Cloud With Falco: Overview And Project Updates - Jason Dellaluce & Luca Guerra, Sysdig Migrating From Single-Node Kubernetes Control Plane To HA In Production - Cong Yue & David Oppenheimer, Databricks When the Logs Just Don’t Cut It: Root-Causing Incidents Without Re-Deploying Prod Cloud-Native WebAssembly: Containerization On the Edge - Michael Yuan, Second State 最後に 番外編:現地の様子をお届け 3年ぶりにアメリカでの現地開催となったKubeCon現況 今回参加してきたKubeConはアメリカのデトロイトで現地+オンラインのハイブリッド開催でした。この形式での開催は、今年の5月にEUで開催されたのに続き、新型コロナウイルス感染症(COVID-19、以下コロナ)のパンデミック以降2度目となっています。3年ぶりのアメリカでの現地開催ということもあり、久しぶりに対面で会う人たちとの再会を懐かしんでいる参加者が多かったように思います。 コロナが完全に収まっていないこともあり、会場内では飲食時以外マスクの着用が義務となっていました。また、以下写真のような缶バッジやシールによって積極的に話しかけて良いかや握手などの接触を許容しているかなどの意思表示ができるよう、感染対策に配慮しつつも参加者の意志を尊重できる工夫がされていました。 KubeConのスケジュールとしては、3日間にわたってキーノートやセッションを見ることができます。また、KubeCon開催前の2日間は同じ会場で共同開催となるEnvoyやKnative、eBPFなどKubernetesやクラウド周りのトピックのカンファレンスも開催されていました。 KubeConではキーノートやセッション、LTなどを通してKubernetesに関する最新のアップデートの紹介や、実際にKubernetesを採用した企業の幅広い運用ノウハウを聞くことができます。以降では参加してきた社員がそれぞれ気になったセッションについて取り上げてご紹介します。 最後に現地の様子もまとめたので、そちらもお楽しみに! 参加メンバーによるセッション紹介 Istio Today and Tomorrow: Sidecars and Beyond SRE部ECプラットフォーム基盤SREブロックの巣立です。 Solo.ioのLin SunとGoogleのMitch ConnrsによるIstioの新しいデータプレーンモデルであるAmbient Meshについてのセッションでした。 Istio Ambient Meshは2022年9月に公開されたばかりです。これまでのIstioではアプリケーションコンテナと一緒に配置されるサイドカーモデルを採用していましたが、Ambient Meshではサイドカーを必要としません。サイドカーモデルでは、サイドカーのアップグレード時にアプリケーションの再起動が必要になったり、アプリケーションとサイドカー間の起動・終了シーケンスが複雑になったりといくつかの課題があります。 また、サイドカーではIstioの基本的な暗号から高度なL7ポリシーまで全てを実装しています。そのためL7の処理を必要としない場合でも、サイドカーコンテナを維持するため過剰なリソースが必要となります。そこでAmbient Meshでは、トラフィックのルーティングを処理するsecure overlay layerとL7 processing layerの2つのレイヤーに分割しました。レイヤーを分割したことでユーザは必要に応じてL7の処理を利用するかどうかをワークロードのニーズによって使い分けることができるようになります。 Ambient Meshを利用することで、コンピュータコストの削減や運用・アップグレードなどのオペレーションの負荷の削減など様々な恩恵を受けることができます。 ( link より引用) また、Ambient Meshやサイドカーで実行されるワークロードは共存可能で、ユーザーはワークロードのニーズに基づいて使用することが可能になります。 ( link より引用) こちらから実際にIstio Admbient Meshを試すことができます。 istio.io また、会場ではLin Sun&Christian Posta執筆のIstio Ambient Explainedが配布されており自分もゲットできました! Ambient Meshは2022年11月時点では本番環境での利用は推奨されていません。2023年の本番環境への移行に向け、現在も活発に開発が行われています。弊社もIstioを利用していますが、サイドカーのアップグレードの度にアプリケーションを再起動させるのはとても面倒に感じていました。Ambient Meshを導入できればアプリケーションの再起動も不要になり運用負荷の軽減が期待できます。今後の動向にも注目していきたいと思います。 Cloud Governance With Infrastructure As Code (IaC) With Kyverno And Crossplane - Dolis Sharma, Nirmata SRE部ECプラットフォーム基盤SREブロックの織田です。 このセッションでは、KyvernoとCrossplaneを用いてIaCでクラウドを管理する方法が紹介されました。 Crossplaneは、クラウドネイティブなコントロールプレーンを構築するフレームワークです。Kubernetesクラスターを拡張することで複数ベンダー(AWS,GCP,Azure)のインフラストラクチャやマネージドサービスのオーケストレーションをサポートします。 Crossplaneを使うことでKubernetes上からAWS EC2やS3などを管理できます。具体的にはDeploymentやJobのようにyamlでmanifestを作成し、applyするだけなので、容易に取り入れることができます。 手元で試したい場合は、 Getting Started で簡単に試すことができます。 Kubernetesにデプロイすることによって、Kubernetesの機能であるReconcileによりAWSのインフラストラクチャやマネージドサービスが定義された状態に維持されます。また、Kubernetesで管理することによって、容易にGitOpsにも対応できることも良い点だと思います。 次にKyvernoでCrossplaneを使って作成したリソースを制限する方法についてです。Kyvernoは、Kubernetesのためのポリシーエンジンです。アドミッションコントロールやバックグラウンドスキャンを利用して、設定のvalidate,mutate,generateを行います。 KyvernoのポリシーはKubernetesリソースであり、yamlで記述するため新しい言語の学習コストは不要です。 Kyvernoを使うことによって、Crossplaneのmanifestに対して制限をかけられます。例えば開発環境ではt2.mediumのような小さなインスタンスのみ構築できるようにする、RDSの特定バージョンより前のバージョンは利用させないようにする等ができるのではと思います。 KubeConでの多くのセッションを通じて、Crossplaneが様々な企業で使われていると感じました。 弊チームではAWSリソースのIaCツールであるCloudFormationを利用しています。そのためCrossplaneに置き換えるかは非常に悩ましいのですが、ReconcileやGitOpsが活用できると考えると乗り換えを再考しても良さそうだと思いました。 “Why Can’t Kubernetes Devs Just Add This New Feature? Seems So Easy!” - Understanding the Feature Lifecycle In Kubernetes - Ricardo Katz, VMware & Carlos Panato, Chainguard SRE部ECプラットフォームサービスSREブロックの出川です。 こちらのセッションは、Kubernetesに新しい機能が提案されてGAになるまで(または途中でreject/rollbackされるまで)にメンテナやコントリビュータの間で何が起こっているかを、具体例をもとに示したセッションです。 機能追加は本当に大変で常に様々な議論が必要になること、また共に議論をしたり実装する人が足りていないために機能追加には時間を要することが述べられていました。 ここで紹介されていた議論の観点として以下が挙げられていました。 Questions to be answered(抜粋): The feature solves a wide problem or is this too niche? (解決するのは広い範囲の問題か、ニッチすぎる問題か?) Does the feature bring (or solve) a security concern? (セキュリティ上の懸念をもたらす/解決するか?) Does the feature bring (or solve) a performance concern? (パフォーマンスに関する懸念をもたらす/解決するか?) Is the feature a breaking change? (破壊的変更か?) Breaking changes in GA are not allowed! (GAに破壊的変更が入ることは許可されていない) Was this feature discussed before? What are the conclusions of previous discussions? (以前議論されたことがあるか? 以前の議論の結論は?) このような観点での議論が、開発フェーズのあらゆるシーンで行われます。下図のように、SIG (Special Interest Groups) との議論が行われます。そしてKEP (Kubernetes Enhancement Proposal) を書くことによるコミュニティ全体への提案が行われます。その後Alpha, Beta, GAそれぞれの前段階で度重なる議論が行われることになります。 ( link より引用) 例えばkubectlコマンドの出力に色を付けたいという素朴なfeature requestがあります。 https://github.com/kubernetes/kubectl/issues/524 「kubectlに色を付けるべきか?」を考えるだけでも、「workaroundはないか、kubecolorではダメなのか」「出力に依存するスクリプトは影響を受けるか」「任意のカラーテーマを実装できるのか」「色盲の人々のアクセスビリティは変わるか」「これを長期的にメンテナンスしていくモチベーションはあるか」など、様々な観点での議論が行われます。 2018年に立てられたこのissueは、2022年11月現在まだクローズに至っていません。 ( link より引用) 他にもNetwork Policyにフィールドを追加してport rangeを指定できるようにする、kubercファイルによる設定(PR)、Ephemeral containers(PR)における例も紹介されていました。 また、どんどん新しいアイデアを上げて欲しい、コードを書くだけがcontributeではない(どんなcontributionも歓迎する)ということも強調されていました。 このセッションは自分にとって、単純にKubernetesとその周辺の開発でどのような議論がなされているかがよくわかり、非常に興味深いセッションでした。Kubernetesのような、既に多くのEnterpriseレベルの本番環境に採用されるほどのOSSはかくあるべきだと感じました。 KubeConはメンテナ等のKubernetesコミュニティの人々が直接会する場所であり、このようなセッションはコミュニティがどのようなことを考えているかが感じられます。KubeConのような場が初めてな自分にとってよい機会となりました。 参考: Kubernetes SIGs https://github.com/kubernetes-sigs Kubernetes Enhancement Proposals (KEPs) https://github.com/kubernetes/enhancements/blob/master/keps/README.md Security In the Cloud With Falco: Overview And Project Updates - Jason Dellaluce & Luca Guerra, Sysdig 計測システム部SREブロックの西郷です。 このセッションではコンテナとKubernetes環境下における脅威検知のデファクトスタンダードであるFalcoについて、直近のアップデートや今後予定されているアップデートが紹介されていました。 今回はその中からいくつかピックアップしたいと思います。 直近のアップデートからは、2022/9/15に発表された gVisorのサポート についてです。 gVisorはGoogleが開発している低レベルコンテナレイヤで、GKEやAppEngine等で利用されています。仕組みとしてはホストのカーネルとコンテナを切り離し、サンドボックス化したコンテナでセキュアにアプリケーションを実行します。この分離により、カーネルモジュールもしくはeBPFでイベントを収集するFalcoはgVisorのイベントを検知できないという課題がありました。今回のサポートによりそれが解消され、例えばGKE SandboxのgVisorを有効にした環境に対してもFalcoの利用が可能になりました。バージョン0.32.1から対応しているとのことなので、詳しくはFalcoの以下ドキュメントを参照ください。 falco.org その他にもバージョン0.33からは同一インスタンス内で複数のイベントソースが処理できるようになったこと等が紹介されていました。これまでは複数のイベントソースからのイベントに対応するには、各イベントソースごとに複数のFalcoインスタンスをデプロイする必要がありました。 ( link より引用) 今回のアップデートにより、よりFalcoが使いやすくなったのではないでしょうか。 また、Upcomingなものとしては新しいeBPF Probeについての話がありました。eBPFの最先端機能を妥協することなく活用した新しいeBPF Probeの開発が進められており、いずれは従来のものと置き換えることが想定されているようです。現時点では80ほどのシステムコールに対応しており、従来のものより安全で高速化するだろうとのことでした。 計測システム部ではこれまでに静的解析の機構として、AquaSecurityによるkube-bench等の導入を進めてきました。まだFalcoは導入できていないのですが、ゆくゆくは動的解析の機構として導入したいと考えているので、今後も動向を注視していきたいと思います。 Migrating From Single-Node Kubernetes Control Plane To HA In Production - Cong Yue & David Oppenheimer, Databricks ブランドソリューション開発本部SREブロックの和田です。 今回取り上げるのは、Databricks社によるKubernetesコントロールプレーンを、単一構成からHA構成へ移行した際のノウハウを紹介するセッションです。Databricks社は2016年にKubernetesを採用しました。当時コントロールプレーンのHA構成は一般的でありませんでした。単一構成の場合、可用性に問題があることはもちろん、Kubernetesクラスタのバージョンアップ等の運用においてロールバックが困難であり運用上の課題があります。そこで、単一のVMで稼働するコントロールプレーンのスナップショットを取ることで複数台のコントロールプレーンを構築し、ロードバランサーを介してHA構成を実現しています。 ( link より引用) この構成により一部のコントロールプレーンに問題が生じた際に、該当のノードへのリクエストを避けることができます。移行時には増設したVMに対してetcdのデータをリストアし、Multi-nodeのetcdクラスターを設定した後にapi-serverの更新を受け付けるようにしています。従ってロードバランサーやetcdを増設するだけではなく、更新するタイミングを気にする必要があり、本セッションでは具体的な手順が紹介されていました。移行後のコントロールプレーンの操作は自動化して、問題が発生した場合はロールバックする仕組みを用いています。 一口にKubernetesと言っても導入時期やシステムの規模によって、直面する課題は様々だと実感しました。今回のようにKubernetesを初期段階で採用した企業の運用ノウハウが聞けるのもKubeConの魅力の1つです。このような運用に関するセッションは「Reliability + Operational Continuity」というカテゴリで確認できます。 When the Logs Just Don’t Cut It: Root-Causing Incidents Without Re-Deploying Prod ブランドソリューション開発本部SREブロックの繁谷です。 こちらのセッションではbpftraceとPixieを用いて、デプロイなしでGoアプリケーションをロギングする様子が紹介されています。 本番環境などデバッガを展開できない環境でのインシデントの調査はログが頼りですが、ログに出ていない箇所のデータを取得する場合はログを出すためのデプロイが必要です。しかし、そのような環境へのデプロイは時間がかかったりリスクがあるなどの問題があり調査に時間がかかってしまいます。そこで紹介されているのが、eBPFをベースとしたbpftraceを用いたPixieによるアプリケーションのロギングです。 eBPFはいくつかの条件を満たすプログラムをカーネル上に動かす仕組みで、カーネルの機能を安全かつ効率的に拡張できます。 ebpf.io bpftrace はkprobesやuprobesなど、カーネルやユーザ空間の様々な箇所にbpftrace Languageで書いた処理を指し込むことができます。 Pixie はbpftraceを独自の言語で使いやすく拡張しつつその結果をビジュアライズしてくれるツールです。bpftraceによるデータの取得に関してGoアプリケーションのロギングもサポートしています。 実際にPixieを動かしてみました。Macの場合は下記のドキュメントに沿ってminikubeをローカルで用意し、そこにアプリケーションをapplyしてPixieへデプロイするだけです。 docs.pixielabs.ai Goアプリケーションのロギングについて以下のデモを確認します。eの近似値を計算する computeE の引数 iterations をデプロイなしでロギングします。 docs.pixielabs.ai github.com デモアプリを立ち上げた状態ではPixieのテーブルに何も表示されませんが、APIを叩いた後に再度ロギングしてみると以下のように引数 iterations の値が得られます。 Pixieのアプリケーションの動的なロギングはGoのみの対応ですが下記の記事によるとC++やRustなども可能とあるので今後の展開に期待できそうです。 blog.px.dev bpftraceのベースであるeBPFについてコミュニティも注目している様子が伺え、KubeCon + CloudNativeConの様々なセッションで取り上げられていました。 先に書かれているFalcoや超満員だったセッションの「 100Gbit/S Clusters With Cilium: Building Tomorrow's Networking Data Plane 」で扱っているCiliumもeBPFが使われています。 今回イベントへ参加してeBPFの学習やコミュニティへの貢献が大変有意義なものになることが実感できました。 Cloud-Native WebAssembly: Containerization On the Edge - Michael Yuan, Second State 計測システム部SREブロックの纐纈です。 今回のKubeConの前半に行われたLTの中で、WebAssembly(以下、WASM)のランタイムであるWasmEdge用のDapr SDKを使ったマイクロサービスアプリケーションが紹介されていました。それを見てWASMとクラウド周りの現状が気になったので事前に調べた上で、こちらのセッションを見に行ってきました。 このセッションでは、LinuxコンテナとWASMの比較から始まり、現在WASMが対応している言語やライブラリ、DockerやKubernetes上で実際に動くデモなどが紹介されました。 WASMはイメージサイズの小ささや立ち上がりの早さ、ネイティブ実行に近いパフォーマンス、デフォルトで高いセキュリティを持つ仕様、言語に依存しないなどの特徴があります。 今回KubeConの隣接イベントであるWASMConで、Technical PreviewとしてDocker上にWASMの実行環境が統合される、という発表がありました。 www.docker.com これによって、AWSやGCPなど他のクラウド上のDocker実行環境でも動かせるようになりました。今までもWASMのアプリケーションはWASMの実行基盤が用意されているwasmCloudというクラウドで動かすことができたのですが、よりWASMの本番利用がしやすくなったと思われます。 またWASM用のDaprのSDKが開発されたことにより、LinuxコンテナとDaprを使用して現在運用されているサービスの中で補完的にWASMのマイクロサービスを使用できるようになりました。 現状WASMは既存のLinuxコンテナの環境を置き換えるものではなく、補完的に利用できる技術だと言えそうです。 最後に メンバー全員初めてのKubeCon参加だったのですが、プロジェクトのメンテナから直接話が聞けたり質問できたりする機会であることを体感でき、非常に貴重な経験になりました。 カンファレンス期間中よく耳にしたフレーズですが、OSSはコミッターなしで成り立ちません。利用する技術を自分達で作ることができるのはOSSの醍醐味であり、普段使っている技術をただ使うのではなく、貢献していくことが大切であることを再認識しました。今回のKubeConで得た知識を業務に活かしつつ、還元していけたらと思います。 ZOZOでは一緒に働くエンジニアを募集していますので、興味のある方は以下リンクからぜひご応募ください。 hrmos.co 番外編:現地の様子をお届け 会場横を流れるデトロイト川、川の向こうにはカナダが見えます。カンファレンス期間中は早朝にRiver Walkのセッションも行われていました。 ランチは毎日メニューが異なるのですが、何より温かいものが出ることに驚きました。これは1日目のカレーのようなものと、ツナサラダ。美味しかったです。 Keynote会場。 会場のHuntington Place外観。 CNCFストアではTシャツやマスク、ぬいぐるみなどの様々なグッズが販売されていました。 最後は参加メンバー全員で撮った集合写真。お疲れ様でした!
はじめに こんにちは。計測プラットフォーム開発本部SREブロックの纐纈です。今年の4月に入社し、ZOZOMATやZOZOGLASSの運用改善に取り組んでいます。また、今年の夏US向けにZOZOFITをリリースしましたが、そちらの機能追加にも今後関わっていく予定です。 計測システムでは最近Argo Rolloutsを導入してカナリアリリース、自動ロールバックを実現しました。本記事では、その具体的な導入方法と効果についてお伝えします。 目次 はじめに 目次 Argo Rollouts導入前のリリースの問題 カナリアリリースの導入 導入後の効果 ツールの選定 Argo Rolloutsについて DeploymentからRolloutへの移行 1. 既存のDeploymentを参照するRolloutリソースを作成して、Podを立ち上げる 2. HPAの対象をDeploymentからRolloutに変更する 3. Deploymentのspec.replicasを0にする Datadogとの連携と注意点 ALBとの連携と注意点 マルチテナントにおける横展開について まとめ 終わりに Argo Rollouts導入前のリリースの問題 Argo Rollouts導入前までは、以下の手順でロールバックしていました。 リリース担当者が異常を検知 リリースPRをリバートする リバートPRをマージする Argo CDが変更を検知し、アプリケーションを旧バージョンに戻す 私達のチームではArgoCDを導入しており、KubernetesリソースをGitHubで管理しています。このため、ロールバックは、アプリケーションのイメージタグを変更するリリースPRをリバートすることで完了します。しかしながら、旧バージョンのPodが起動し、トラフィックが流れるまでの間、ユーザー影響は避けられません。 こうした理由からユーザ影響を最小限に抑えるため、リリース戦略を見直す必要がありました。 上記の課題を解決するため、弊チームではカナリアリリースを導入することにしました。 カナリアリリースの導入 まずカナリアリリースについて簡単に説明します。カナリアリリースは、リリースの手法の1つです。初めからリリースするバージョンを全体に公開するのではなく、公開する対象を一部のユーザーだけに絞りつつテストを行い、定められた基準を満たした場合のみ全体に公開するリリース方法のことを指します。これによって、ユーザーのトラフィックありきのテストを本番環境で行いつつ、障害発生時にもユーザーへの影響を最小限にしながら自動ロールバックを実現できるようになります。 また段階的なリリース方法としてカナリアリリースとよく比較されるのが、Blue/Greenデプロイです。Blue/Greenではなくカナリアリリースを選択した理由は、実際にユーザートラフィックが流れる点にあります。カナリアリリースでは実際にユーザートラフィックが流れている状態でテストを行うため、Podの立ち上げ時にはわからない問題も対応できます。計測システムでは、ZOZOTOWNのAPIサーバーと接続して認証するため、実際にトラフィックを流してのテストを行う必要がありました。 導入後の効果 Argo Rolloutsを導入してカナリアリリースを実現してからは、より安心安全にリリースできるようになりました。リリース担当者の心理的負荷が減っただけでなく、エラー検知した際もPodが瞬時に入れ替わるため、障害による影響範囲を最小限に抑えられます。これによって、本番リリース直後のアラートに怯える必要性が大幅に緩和されました。 また副次的な効果として、今回ステージング環境にも同様にArgo Rolloutsを入れたため、エラーを伴う変更が本番環境でリリースするまでに発見できるという効果もありました。ステージング環境ではユーザーのトラフィックは流れていませんが、暖機運転と共に動作確認用の処理がPod起動時に走るため、エラーがあればこの動作確認で発見できます。 この時点でエラーを伴う変更を検知して自動ロールバックが走ったため、ユーザー影響を出す前にステージング環境で食い止めることができました。 カナリアリリースでの1つデメリットとして、Podが徐々に入れ替わって段階的にテストを行うため、全体的なリリース時間が伸びてしまうという特徴があります。例えばリリース後に動作確認やチェックを行う場合、リリース担当者が全てのPodが入れ替わるまで待つ必要があり、これはリリース担当者のリリースに費やす時間的制約を増やしてしまいます。そのため、できる限りリリース後の作業は自動化できると、よりカナリアリリースの恩恵を受けられるようになるかと思います。 ツールの選定 カナリアリリースの特徴や効果については一通り説明できたと思うので、なぜ今回Argo Rolloutsを選んだのかについて説明していきます。カナリアリリースは通常のKubernetesリソースであるDeploymentではサポートされておらず、外部ツールを使う必要があります。カナリアリリースをサポートする外部ツールはArgo Rollouts以外にも、 Spinnaker や Flagger のようなものがあります。Flaggerについては社内の他チームで採用され、ブログ記事も公開されているので、気になる方はこちらの記事をご参照ください。 techblog.zozo.com 今回計測システム部でArgo Rolloutsを選んだ理由は3つあります。 まず、計測システムでは既にArgoCDの導入が済んでおりArgo Rolloutsとの親和性が高かったこと。これはArgoCDのWeb UI上でArgo Rolloutsの操作ができる、Slack通知用のトークンなどを共通して使えるなどの利点があります。 次に、Argo Rollouts以外のツールで必要となるIstioのようなサービスメッシュがまだ導入されていないこと。現在計測システムでは、サービスメッシュは使われておりません。そのため、カナリアリリースのためにサービスメッシュを導入すると、工数も増えるため導入のハードルが上がりました。 最後に、必要最小限の機能であるDatadogやCloudWatchの連携ができること。今回考慮したツールの中ではどれもサポートされていましたが、もしArgo Rolloutsがサポートしていなければ、選定されることはありませんでした。 Argo Rolloutsについて Argo RolloutsはBlue/Greenやカナリアリリースのようなプログレッシブデリバリーをサポートしている、Kubernetesコントローラーです。CRDs(Custom Resource Definitions)でもあります。監視ツールと連携させて自動ロールバックや段階的リリースを行なったり、Ingressコントローラーやサービスメッシュと連携させて限定公開時のトラフィック制御ができます。 連携できる監視ツールはDatadog、CloudWatch、Prometheusなどがあります。事前に定めた基準から外れたメトリクスを検知した場合、自動でロールバック、基準内だった場合は段階的にリリースします。また、このリリースの段階やテスト項目などはKubernetesのマニフェストで定めることができます。 限定公開時のトラフィックの制御については、ALB IngressコントローラーやIstio、nginxなどと連携できます。連携する場合パーセント単位で制御できますが、連携しない場合でもPod数単位での段階的な切り替えが可能です。 カナリアリリース用にArgo Rolloutsで必要なリソースは、RolloutとAnalysis Templateの2つです。 RolloutはArgo Rolloutsのカスタムリソースで、Deploymentリソースの項目に追加してカナリアリリースやBlue/Greenデプロイ用のフィールドが追加されています。DeploymentのExtensionだと考えてもらえれば良いです。Deploymentに含まれているフィールドに関しては対応されている他、カナリアリリースやBlue/Greenデプロイの細かい設定ができます。また、既存のDeploymentを参照して設定を読み込むこともできます。 Analysis Templateでは、Datadogなどの監視ツールで取得するメトリクスとテスト内容が記述できます。テスト内容は、頻度や取得したメトリクスのテスト条件、ロールバックまでの許容エラー数などが記載できます。 DeploymentからRolloutへの移行 Argo Rolloutsを導入すると、PodをRolloutリソースで管理することになります。新規プロジェクトで導入するのであれば、最初からDeploymentではなくRolloutリソースを作成すれば良いです。一方、既存のプロジェクトで導入する場合はDeploymentリソースから移行する必要があります。 今回弊チームでは、DeploymentからRolloutへ障害なく移行できたのでその方法を説明していきたいと思います。 Argo Rollouts導入前の計測システムでは、DeploymentでPodの設定、HPA(Horizontal Pod Autoscaler)でPod数を管理していました。 既存のDeploymentを参照するRolloutリソースを作成して、Podを立ち上げる HPAの対象をDeploymentからRolloutに変更する Deploymentの spec.replicas を0にする 以上のステップに切り分けてリリースすることで、移行は完了します。詳しく見ると、以下のようになります。 1. 既存のDeploymentを参照するRolloutリソースを作成して、Podを立ち上げる まず、Rolloutリソースを用意します。この時点ではまだDeploymentからのPodは立ち上げたままにしているため、DeploymentとRollout両方からPodが立ち上がっている状態です。 Rolloutでは、このようにDeploymentを参照することでDeploymentの設定を読み込むことができます。Deploymentのイメージタグなどが変更された際、Rolloutが変更を検知して、デプロイを行います。 apiVersion : argoproj.io/v1alpha1 kind : Rollout ... spec : workloadRef : apiVersion : apps/v1 kind : Deployment name : <name for Deployment> 2. HPAの対象をDeploymentからRolloutに変更する 次に、オートスケールを利用してる場合は、オートスケールの対象をRolloutリソースに変更します。 apiVersion : autoscaling/v2beta2 kind : HorizontalPodAutoscaler ... spec : scaleTargetRef : apiVersion : argoproj.io/v1alpha1 kind : Rollout name : <name for Rollout> 3. Deploymentのspec.replicasを0にする 無事RolloutからPodが立ち上がることを確認したら、DeploymentからのPodを落とします。これは、Deploymentのreplicasを0に設定することで実現できます。また、revisionHistoryLimitを0にすることによって、明示的な設定がない場合デフォルトで10個保持されてしまうreplicasetを削除できます。 apiVersion : apps/v1 kind : Deployment ... spec : replicas : 0 revisionHistoryLimit : 0 今回移行した方法以外にも、以下の方法でもDeploymentからRolloutへの移行はできます。 新規でRolloutリソースを作成して、Podを立ち上げる。この時点でPodが倍になる。 Deploymentの spec.replicas を0にする。この時点でRolloutに紐づいたPodだけになる。 問題なければDeploymentリソースを削除する。 どちらの方法を取るかはこの方法だとDeploymentを完全に削除できる一方、Rolloutの中にDeploymentのテンプレートを入れるためRolloutの中身が煩雑になってしまいます。既に動いているDeploymentがあるのであれば、そちらを参照する方がそれぞれのテンプレートがシンプルになり管理しやすいかなと思います。どちらの方法でも移行の手間は変わらないです。 Datadogとの連携と注意点 今回計測システムでは、Datadogと連携させてメトリクスを取得しています。Datadogとの連携はシークレットを登録するだけで可能です。 apiVersion : v1 kind : Secret metadata : name : datadog type : Opaque data : address : https://api.datadoghq.com api-key : <datadog-api-key> app-key : <datadog-app-key> Argo Rollouts内のコードを読むと、Datadogとの連携部分では メトリクスのAPI を叩いていることがわかります。なので、Datadogのログなどは通常取得できません。しかし、多くの場合ログからエラーなどを取得したいところだと思います。幸いなことにDatadogには、ログのクエリをメトリクスとして保存する機能があります。これを利用すると、Argo RolloutsからもDatadogのログを参照できます。 docs.datadoghq.com ALBとの連携と注意点 Argo RolloutsはALBとの連携をサポートしているため、今回計測システムで使用しているALB Ingress Controllerとの連携を検討しました。しかし、最終的にトラフィック制御はしないことにしました。 Argo Rolloutsではロードバランサーやサービスメッシュなどと連携せずに、Pod数単位でユーザートラフィックを分散させるという選択肢があります。この場合先述した通り、ユーザーのトラフィックをパーセンテージ単位でリリースバージョンに流すのではなく、純粋にPod数単位での分散となります。例えば新しくリリースするバージョンのPodを1つ、リリース前のバージョンのPodが3つ立てている場合、25%のトラフィックがリリースバージョンに流れることとなります。 本来であればALBと連携させてリリースするバージョンにはユーザートラフィックをパーセンテージ単位で絞って、障害時のユーザー影響を極小化したいところなのですが、1箇所対応できない点がありました。 現状Argo Rolloutsの仕様として、RolloutとALB Ingressは1対1である必要があり、ALB Ingressが複数ある場合は想定されていません。ALB Ingressが複数ある場合、RolloutもALBの数に合わせて作成する必要があります。そうなるとパスごとにデプロイのタイミングを考える必要があり、ロールバックが発生する場合も考慮するとバージョンがパスによって異なるといった問題が起きます。運用上Podの管理が大変になることもあり、今回ALBとの連携は断念しました。 計測システムが複数のALB Ingressを使っているのは、クライアントによって接続するプロトコルが異なり、1つのALB Ingressで複数のプロトコルには紐付けられないためです。具体的には、ネイティブアプリからはgRPC、ZOZOTOWNサーバーからはRESTが使用されており、IPによってリスナーを分けています。また、RESTでの通信も計測システム内のEnvoyでgRPCに変換されるのですが、gRPCを使用している詳しい背景については以下の記事をご参照ください。 techblog.zozo.com マルチテナントにおける横展開について マルチテナント構成を取っている場合、Argo Rolloutsを他のプロダクトに導入するのは非常に簡単です。計測システムでは、ZOZOMATやZOZOGLASSを同じクラスタ内で運用するマルチテナント構成をとっています。そのため、ZOZOMATに次いでZOZOGLASSでもArgo Rolloutsを導入しましたが、少ない工数で横展開できました。 マルチテナント構成に関する詳しい背景などは、こちらの記事をご参照ください。 techblog.zozo.com 1つ目のプロダクトに導入する際は、事前にArgo Rolloutsリソース自体のインストールやSlack通知の設定、Datadogなど連携するツールの設定などを行う必要があります。ですが、2つ目以降は、構成が変わらない場合RolloutとAnalysisTemplateのみの追加で導入が可能です。ここはマルチテナント構成を取っている弊チームでは大きな利点となりました。 まとめ Kubernetes上でカナリアリリースを実現するために、Argo Rolloutsは有用で楽に導入ができるツールでした。 カナリアリリースができるようになり、弊チームではより安全に定期リリースを行えるようなり、リリース担当者の負担も軽くなりました。 本記事がArgo Rolloutsの導入やカナリアリリースを検討している方への手助けになれば幸いです。 終わりに 計測プラットフォーム開発本部では、今夏に新しいサービスである ZOZOFIT をUSにてローンチしました。更にスピード感を持った開発が求められますが、このような課題に対して楽しんで取り組み、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、SRE部ECプラットフォーム基盤SREブロックの織田です。普段は主にZOZOTOWNのリプレイスやインフラを改善、運用しています。 本記事では、Secret管理コンポーネントであるKubernetes External Secrets(以降、KESと表記)の非推奨を受けて、どのような対応を実施したのか紹介します。 目次 はじめに 目次 なぜSecret管理コンポーネントを利用するのか? Kubernetes External Secrets(KES)について KESとは? KESを使ったKubernetes構成 KESの非推奨について 移行先の選定 優先事項の決定 移行先の検討、比較 移行先の決定 External Secrets Operator(ESO)について ESOのアーキテクチャ 移行における注意点や変更点 移行の実施 移行方法 工夫したポイント 最後に なぜSecret管理コンポーネントを利用するのか? Secret管理コンポーネントとは、KubernetesのSecretを管理する機能を持つソフトウェアのことです。 Kubernetesを運用する上でSecret管理コンポーネントを利用しない場合は、Secret manifestをKubernetesにapplyしSecretを作成することになるかと思います。KubernetesのSecretは、データをbase64エンコードしているだけなので、誰でもbase64デコードができ、データを確認できます。そのため、Secret manifestのような基本的に秘匿情報を含むファイルは、GitHubのようなバージョン管理システムにコミットするべきではありません。 コミットを避ける方法の1つとして、Secret管理コンポーネントの利用が挙げられます。そうすることでAWS Secrets ManagerやAzure Key VaultのようなSecret Management System(以降、SMSと表記)に登録された秘匿情報をKubernetesが取得し、Secretに注入できます。その結果、Secret manifestをバージョン管理システムにコミットせず、安全にデータを扱うことが可能です。 弊チームではこれまでSecret管理コンポーネントとしてKESを使用していました。 Kubernetes External Secrets(KES)について KESとは? KubernetesのSecretリソースを管理するオペレーターです。弊チームでは外部のSMSからデータを取得し、取得したデータをKubernetesのSecretへ注入するために利用しています。 KESを使ったKubernetes構成 弊チームではどのような構成でKESを利用していたのか紹介します。下図がKESを採用していたおおよそのKubernetesの構成図です。 KESがExternalSecretの内容をもとにAWS Secrets Managerに登録されているデータを取得し、Secretを作成します。 Application(Deployment,Cronjob,etc.)は、ExternalSecretが作成したSecretを参照したり、volumeとしてマウントします。 このようにSMSから取得したデータをKubernetesで利用することによってSecret manifestをコミットせずに済むため、よりセキュアに扱うことが可能です。 KESの非推奨について 2021/11/03に KES は 非推奨化 が発表され、現在はアーカイブされています。 KESが非推奨になった背景としては、アクティブなメンテナが存在していなかったため、プロジェクト内の問題を引き起こしている技術的負債が解消されず、メンテナンスされていないことだと KES is deprecated, migrate to ESO! で言及されています。 発表の内容を見るとOSSをメンテナンスすることはモチベーションの維持や管理の面においてとてもハードだと感じるとともに、今まで利用させていただき感謝しかありません。 ただ弊チームのKubernetes Clusterでは、全SecretをKESで生成しているため、なにかしらの対応をとる必要がありました。 移行先の選定 ここからは移行先コンポーネントの選定について具体的に説明します。 優先事項の決定 まずは移行先を決めるにあたって、比較する項目を決め以下の4つを優先事項としました。 Kubernetes manifestやkustomizeの構成に変更が少ないこと Secret更新時の運用に変更が少ないこと 移行コストが少ないこと KESで利用している機能、または同等の機能が使えること ある程度アクティブに活動があるOSSであること 現状、KESに不満はないため、kustomize構成やSecret運用に大きな変更を加えず、移行コストを抑えたいと考えました。 KESで利用している機能は、Secretの更新タイミングをコントロールできるもの(自動更新のOFF)です。自動更新をONにしているとSMSを更新後、Secretも自動更新されてしまうため、リリースタイミングをコントロールしたいと考え、自動更新をOFFにしています。Secretを更新してもPodを再起動しなければ更新後の値を読み取りませんが、タイミング悪くPodが再起動されてしまった場合意図せず読み込んでしまうため、このような対応をしています。 移行先の検討、比較 次に移行先の検討です。アクティブに開発されていそうな以下3つのコンポーネントを検討し、比較しました。 External Secrets Operator Secrets Store CSI Driver Sealed Secrets External Secrets Operator(以降、ESOと表記)は、KESを開発していたオーガナイゼーション(External Secrets)が開発しています。JavaScriptで書かれていたKESをGoでリファクタリングしたコンポーネントです。ExternlSecretのapiVersionは変更になっていますが、ExternalSecretがSecretを作成するということは変わっていないため、Kubernetes manifestの変更は少なく済みそうです。KESの 非推奨issue でもESOへの移行を勧められているのとKESからESOへの 移行ツール (以降、移行ツールと表記)が用意されています。そのため、移行コストについても少なく済みそうだと考えました。 Secets Store CSI Driverは(以降、CSI Driverと表記)、 Kubernetes SIGs が開発しています。名前の通り、 Container Storage Interface(CSI) を利用し、シークレットやキー、証明書をPodにVolumeとしてマウントします。また、マウントしたものをSecretに同期したり、複数のSecretオブジェクトを1つのVolumeとしてマウントするなど様々な機能があります。Podにシークレットやキー、証明書をVolumeとしてマウントした上でSecretに同期する必要があることを考えると、少なくともKubernetes manifestへのインパクトは大きそうです。 Sealed Secretsは、 Bitnami が開発していて、"全Kubernetes configをGitで管理できるようにする" ことを目的に作成されました。SecretをSealed Secretsに暗号化させ、Kubernetes manifestを生成します。Kubernetesで動作するコントローラのみが復号化でき、原作者でさえもSealedSecretからオリジナルのSecretを取得できません。そのため、Public repositoryにコミットしても安全に保管できると言われています。 1 この段階で移行先がほぼ決まっている感じはありますが、候補とした3つを比較しました。 2 比較項目\コンポーネント ESO CSI Driver Sealed Secrets Kubernetes manifest変更の少なさ ◎ ✗ △ 移行コストの少なさ ◎ ✗ ○ Secret運用への影響の少なさ ◎ ✗ ○ 累計コミット数 2011 1062 996 自動更新OFF/ON機能 ○ - △ 移行先の決定 比較の結果、移行先は優先事項を全て満たしていたESOに決定しました。比較表を見ると一目瞭然ですが、不採用としたコンポーネントごとの理由としては以下になります。 CSI Driverは、ほかのコンポーネントと比べKubernetes manifestに大きな変更が必要で、それに伴い移行コストや運用変更コストも多そうだと判断したため Sealed Secretsは、SMSを利用できないこと、暗号化した秘匿情報をバージョン管理システムにコミットするのは可能であれば避けたかったため 移行コストやKubernetes manifestの変更コストを優先事項に挙げるとKESの後継であるESOを選ぶのは必然だったのかと思いました。 External Secrets Operator(ESO)について 移行先を決めたのでESOについて見ていきたいと思います。 ESOは、Kubernetesオペレータで外部のSMSから情報を読み取り、その値をKubernetes Secretに自動的に注入します。目的は、SMSとKubernetes Secretを同期させることです。 ESOのアーキテクチャ 全体像としては、下図のようになっています。 Kubernetesをカスタムリソース(CR)で拡張し、ExternalSecretにSecretの保存場所と同期方法を定義することで、外部のSMSからデータを取得しSecretにデータを注入します。参照しているSMSが変更された場合、Kubernetes Clusterの状態を確認し、それに応じてSecretを更新します。 より詳細な情報については、 External Secrets Operatorの公式ドキュメント をご確認ください。 移行における注意点や変更点 移行方法の紹介の前にKESからESOへの移行において、把握しておくべき変更点や注意点を確認します。 API追加に伴うアーキテクチャの変更 Kind:ExternalSecret のapiVersion変更 KESからESOへ移行する際に注意すること 1つ目はAPI追加に伴うアーキテクチャの変更です。KESとESOはカスタムAPIの数が異なり、以下の表の様になっています。 コンポーネント API KES ExternalSecret ESO ExternalSecret, SecretStore, ClusterSecretStore, ClusterExternalSecret KESでは、前述の通りExternalSecretの設定をもとにSMSからデータを取得し、Secretに注入します。ESOでは、新たにSecretStore,ClusterSecretStore,ClusterExternalSecretが追加され、基本的なアーキテクチャとしては下図のようになっています。 ESOは、SecretStoreまたはClusterSecretStoreに格納された情報を使い、外部のSMSに接続しExternalSecretの設定をもとにデータを取得します。無事、SMSとExternalSecretが同期できていれば、取得したデータを注入したSecretが作成されます。というようにSMSとExternalSecretの間にSecretStoreが追加されました。SecretStoreはnamespaceスコープで、ClusterSecretStoreはclusterスコープです。ExternalSecretとClusterExternalSecretも同様です。そのため、移行する際には新しくSecretStoreを作成する必要があります。 2つ目は、 Kind:ExternalSecret のapiVersion変更についてです。KESでは kubernetes-client.io/v1 でしたが、ESOでは external-secrets.io/v1beta1 に変更されています。apiVersionが変わりそれに伴いスキーマも変更になっているため、例えばKESではDeploymentで定義していたデータ取得のポーリング間隔をESOではExternalSecretで定義します。他にも変更になった点があるため、事前に確認が必要です。 ESOへ移行するにあたってどのような変更をしたらよいかについては、 公式ドキュメントのExternalSecretのページ をご確認ください。 3つ目は、KESからESOへ移行する際の注意点です。 Upgrade from KES to ESO でKESとESOではいくつか非互換性があるため、かなり厄介だと述べられています。移行ツールは、非互換性に関してほぼカバーされているとのことですが、移行ツールでは、SecretStoresがKESのService Accountを指定するようになっているため、移行後にKESをアンインストールする予定がある場合は、調整する必要があるとのことです。 移行の実施 ここからは、実際にどのように移行したか紹介します。 移行方法 まずは、移行方法についてです。前提として、今回の移行ではExternalSecret、Secretを既存の名前とは異なる名前で再作成しています。そのため、既存Secret名をそのまま利用したい場合は、以下の手順ではエラーになってしまいますのでご注意ください。 まずは、ESOのドキュメントを見てアーキテクチャを理解しながら移行手順を考えました。幸いなことに移行ツールが用意されていたため、テスト環境で移行ツールを実行したり、 手動移行 を参考にし、移行手順を作成しました。 次に、考えた移行手順を検証し、システムに影響がなく可能な限り作業量の少ない方法を試行錯誤しながら移行手順をブラッシュアップしました。システムに影響を与えないことは必須だと思いますが、弊チームの場合、Secretの切り替え作業が1回では済まないためできるだけ作業量を減らせるよう努力しました。 それらを踏まえた移行手順が以下になります。 ESO(CRD,Deployment,ServiceAccount,etc.)をデプロイ SecretStore,ExternalSecret(ESO)を作成 ExternalSecret(KES)を削除 KES(CRD,Deployment,ServiceAccount,etc.)の削除 各手順について少しずつ触れたいと思います。 ESO(CRD,Deployment,ServiceAccount,etc.)をデプロイでは、カスタムAPIリソース以外をデプロイしました。CRDがKubernetesに登録されていない状態でExternalSecretなどのカスタムAPIリソースを作成しようとするとエラーになるためです。 SecretStore,ExternalSecret(ESO)を作成では、CRD作成後、カスタムAPIリソースをデプロイしました。このタイミングでSecretStore、ExternalSecret、Secretが作成されます。また、KESのExternalSecretをもとに作成されているSecretを参照しているリソース(Deployment,CronJob,etc.)はここでESOのものに切り替えました。KESとESOでは、ExternalSecretのapiVersionが異なるため、作成時の重複についてあまり気にする必要はありません 3 が、より安全に行いたい場合は、KES Deploymentのreplicasを0にしておくことで予期せぬExternalSecretの再作成を防ぐことができます。 ExternalSecret(KES)を削除では、手順2で切り替え、参照されなくったExternalSecretを削除しました。 KES(CRD,Deployment,ServiceAccount,etc.)の削除では、不要になったKES関連のリソースの削除しました。 より安全な移行が求められる場合は、 Manual Migrationで示されている手順 を実施することで解決ができるかと思います。 工夫したポイント 最後になりますが、非推奨の対応で工夫したポイントを2つ紹介したいと思います。 1つ目は、移行手順の策定において公式が用意していた移行ツールを参考にし、安全な移行方法を決定しました。移行用ツールが用意されているのでそちらを利用すればよさそうではあるのですが、以下の理由で利用しませんでした。 移行ツールを利用するには、ツールを修正する必要があった 移行ツールで生成したmanifestを変更するより、現状のmanifestを変更したほうが作業量を少なく済ませられた Upgrade from KES to ESO で言われている通り、移行ツールを実行しても手作業が発生するため(手順のみ参考にさせていただいた) 2つ目に、ESOのリソースとKESのリソースでnameやlabelsなどにおいて名前を被らせないようにしました。どちらもデフォルトの状態で利用しようとするといろいろなリソース名(Deployment,ServiceAccount,etc.)がバッティングしてしまいます。 例えば、ServiceAccountのリソース名はどちらも external-secrets です。ESOをデプロイするタイミングでKESのSAが更新されてしまい、KESをアンデプロイするタイミングでESOも利用しているSAが削除されてしまいます。そのため、KESが利用している名前とはかぶらないように調整し、ESOをデプロイしました。 最後に 本記事では、KESの非推奨対応についてどのようなことを実施したか、具体的に紹介しました。結果として、KESの非推奨対応を行い、ESOに移行することでSecretを管理する重要なコンポーネントで問題が起きる前に対応できました。 また、ESOを調べることで新機能やアーキテクチャの変化によって、Secretをよりセキュアに扱えることや現状抱えているkustomizeのSecret構成を改善できることがわかりました。しかし、今回の対応ではそこまで踏み込んだ対応できていないため、次のステップではそれらの対応を実施すると共にSREとしてトイルを削減したいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://github.com/bitnami-labs/sealed-secrets#sealed-secrets-for-kubernetes ↩ 2022年10月20日現在 ↩ 前提として、KESとESOのExtenalSecretとSecretは別名で作成すること ↩
こんにちは! 計測プラットフォーム本部でiOS/Androidアプリ開発をしている寺田( @tama_Ud )です。 2022年10月5日から7日にかけて、「 DroidKaigi 2022 」が開催されましたね! ZOZOはGOLD SPONSORとして協賛し、オフライン会場にてスポンサーブースの出展をしました。 technote.zozo.com 今回、自分は現地参加が叶いましたので、オフライン参加レポートをお送りします。 目次 目次 ZOZOエンジニアが1名登壇しました ZOZOスポンサーブースの紹介 CfSネタ出し会とレビュー会を実施しました After DroidKaigiを開催しました セミナー・カンファレンス参加支援制度 最後に ZOZOエンジニアが1名登壇しました ZOZOTOWNのAndroidエンジニアを務める鈴木( @s1u2z1u3ki )が、Android Vitalsについて登壇しました! 2022年3月に公開された Google Play Developer Reporting API の解説や、弊社でのクラッシュ・ANR指標の維持・改善について、実例を交えて紹介しています。 speakerdeck.com ZOZOスポンサーブースの紹介 前述の通り、ZOZOはGOLD SPONSORとして協賛し、オフライン会場にてスポンサーブースを出展しました。 また、ノベルティとして「ZOZO特製温泉タオル」を配布しました。オフライン会場入口でノベルティ入りのサコッシュとともに受け取っていただいたかと思います。 #DroidKaigi の会場にお越しいただいている方は入口でサコッシュを受け取りましたか? ZOZOのノベルティとして白地にカラーのZOZO特製温泉タオル♨が入っています。 雨に濡れた身体を拭くも良し、一日の疲れを温泉や銭湯で癒やすときに使うも良し、ぜひ活用ください✨ pic.twitter.com/Kj9Ate6Z05 — ZOZO Developers (@zozotech) October 5, 2022 さらに、ZOZOGLASSとZOZOMATに加え、その場でZOZOGLASSの計測を試していただいた方にZOZOオリジナルTシャツをプレゼントしていました! 他にもステッカーやボールペンなどノベルティが盛りだくさんでした。 ZOZO社員が交代制でブースアテンドをしていましたが、会場は大賑わいでしたね! Androidエンジニアの熱量を肌で感じられました。ZOZOブースにも連日たくさんの方にお越しいただきました。エンジニアトークに花が咲いたり、ZOZOGLASSでパーソナルカラーを計測していただいたりと、ZOZOをもっと知ってもらうきっかけになったかと思います。お越しいただいた皆様、この場をお借りしてお礼申し上げます! CfSネタ出し会とレビュー会を実施しました ZOZOTOWNアプリ部の岩田( @iwata_n )です。ZOZOTOWNに関わるAndroidエンジニアは、週1回集まって技術的な活動をする時間を設けています。それを活用して、CfSに提出するネタ出し会をメンバーみんなで実施しました。 まずは下図のようなマンダラチャートを使用して、ネタの発散から行いました。すべてを埋めることは難しいのですが、ウケそうな話や、実際に自分たちも聞いてみたい話などをざっくばらんに書き出しました。 次に、マインドマップを使ってネタの内容を整理し、具体的なネタに落とし込みました。 ある程度ネタを整理したところで、それぞれのネタに担当者を決めます。チームで相談しながら原稿を作成しました。また、DroidKaigi公式の相談会に参加したメンバーが、より良い応募内容について以下のような情報を収集してくれました。 共感者を増やす 現状の問題に言及する 例:「こういう問題、よくあるじゃないですか?」 発表によって、誰の何が解決するかを具体的に示す なぜその技術・テーマが大事なのかを説明する 例:「Vitalsはなぜ重要なのか?」-> ビジネス指標に影響するから 話すことの概要・アジェンダを具体的に列挙しておく 話す側と聴く側のギャップを防げる これらの知見を活かし、さらに内容をブラッシュアップしました。原稿はGoogle Docs上で作成しました。メンバー全員がコメントを書ける状態に設定することで、活発な議論ができました。 最終的に12件のCfSを提出し、1件が採択されました。出した件数に対して採択の件数がいまいち奮いませんでしたが、ZOZOとしては過去一番の応募件数になりました。今後、さらに採択数を増やせる伸びしろを確認できたと思います。 After DroidKaigiを開催しました フロントエンド部 FAANSにてテックリードをしている堀江 ( @Horie1024 ) です。 昨年に引き続き今年もLINE、ヤフー、ZOZOの3社合同でAfter DroidKaigiをオンラインで開催しました!コードレビューチャレンジの問題解説(LINE)、Androidアクセシビリティに関する取り組み(ヤフー)、LiveOpsの活用(ZOZO)について各社LTがありどれも興味深い内容でした。 ZOZOの下川が発表したLiveOpsの活用に関するスライドはこちらをご覧ください。 speakerdeck.com その後、パネラーの方々とDroidKaigiをメインテーマとしてパネルディスカッションを行いました。Miroのボードを共有しながら、小テーマに沿ってトークを進めました。面白いと思ったセッションを複数挙げられる方もおられ、ボードは付箋でいっぱいになりました。 オフライン参加されたパネラーの皆さんも、対面コミュニケーションが取れることをメリットとして語っておられた印象で、改めてオフライン開催の良さを感じました。After DroidKaigiも引き続き開催していきたいです。 見逃した方はこちらで録画を配信しています! www.youtube.com セミナー・カンファレンス参加支援制度 ZOZOにはエンジニアのイベント参加をサポートする制度があります。 corp.zozo.com ZOZOはGOLD SPONSORとして協賛しているのでスポンサーチケットがありますが、チケット費用をはじめとし、以下の補助が受けられます。 希望するエンジニアは業務として参加 チケット費用、交通費は経費精算、遠方の場合は出張扱い可 (イベントが休日開催の場合)休日出勤扱いで参加 今年は実際に遠方から参加したAndroidエンジニアもおり、私自身もチケット費用の個人負担はありませんでした。これらの制度があることで、イベントに参加しやすい環境が整っていると思います。 最後に 現地で参加された方も、オンラインで参加された方も、お疲れさまでした! また、スタッフの方々もイベント終了までしっかりサポートして頂きありがとうございました。Androidエンジニアにとって久々のオフラインイベントで、多くの方々と交流でき、みなさん満足度が高かったのではないでしょうか? ZOZOでは、Androidエンジニアをはじめ、一緒に楽しく働く仲間を募集中です。ご興味のある方は下記採用ページをご覧ください! hrmos.co
はじめに こんにちは。ブランドソリューション開発本部 バックエンド部 SREの笹沢( @sasamuku )です。 ZOZOではショップスタッフの販売サポートツール「FAANS」を2022年8月に正式リリースしました。FAANSはアパレルのショップスタッフ様を支援する様々な機能を提供しています。例えば、ZOZOTOWN上で実店舗の在庫取り置きができる機能や、コーディネート投稿の機能などがあります。投稿されたコーディネートはZOZOTOWNやWEAR、Yahoo!ショッピングに連携が可能で、今後はブランド様のECサイトとも連携できる予定です。これによりお客様のコーディネート選びをサポートし購買体験をより充実したものにします。機能の詳細に関しましては下記プレスリリースをご覧ください。 corp.zozo.com 今回はFAANSで採用しているワークフローエンジン「 Argo Workflows 」について、その採用理由や構成例をご紹介します。ワークフローエンジンの採用を検討している方、Kubernetesネイティブなワークフローエンジンに興味をお持ちの方の参考になれば幸いです。 なお本稿は単体でもお読みいただけますが、過去の記事をご覧いただくとFAANSの技術変遷をよりご理解いただけます。 techblog.zozo.com techblog.zozo.com 目次 はじめに 目次 ワークフローエンジン選定の過程 ワークフローエンジン導入の背景 代表的なワークフローエンジンの紹介 Kubernetesネイティブなワークフローエンジン Kubernetesネイティブとは ワークフロー定義をマニフェストで管理できる ワークフローエンジンの移行性が高い リソース利用効率が高い Argo Workflowsの選定理由 Argo Workflowsの構成 Argo Workflowsのコアコンセプト 全体構成 ワークフロー実行の流れ マニフェストの分散管理 Workflowの構成 Webコンソールの紹介 ログイン画面 Workflow一覧 Workflow詳細 WorkflowTemplate一覧 WorkflowTemplate詳細 CronWorkflow一覧 CronWorkflow詳細 まとめ おわりに ワークフローエンジン選定の過程 本章では、Kubernetesネイティブなワークフローエンジンに対する考察を交えながら、Argo Workflowsの導入を決めた過程をご説明します。 ワークフローエンジン導入の背景 FAANSが提供する機能の1つに「成果確認」があります。これはショップスタッフ様が投稿したコーディネートに対する閲覧数や経由売上といった指標を成果として可視化するものです。この機能の裏側ではそれらのデータを集計するワークフローが必要となりますが、実装当時のFAANSにはワークフローを実行する基盤はまだありませんでした。 どのツールでワークフローを実行するか検討するために、まずFAANSに求められる最低限の要件を書き出しました。それが下記になります。 スケジュール実行 リトライ機構 タスク間依存関係の表現 Webコンソールでの手動実行やログの確認 1 スケジュール実行やリトライ機構が備わっていることは大前提です。加えて、複雑なワークフローの構成にはタスク間の依存関係を表現できなければなりません。タスク間の依存関係とは、例えば「タスクAとBの完了を待ってからCを実行する」というものです。さらに開発者体験も重要です。ログの確認やワークフローの再実行はWebコンソールでできると便利です。 よって、これらの要件を満たすワークフローエンジンを選定する必要がありました。 代表的なワークフローエンジンの紹介 まずは、フラットな視点で代表的なワークフローエンジンを調査しました。スター数が1kを超えるワークフローエンジンをいくつか抜粋すると以下のようなものがあります 2 。()内は2022/10時点のスター数になります。 Luigi (16k) Apache Airflow (27.7k) Tekton Pipelines (7.4k) Argo Workflows (11.8k) Digdag (1.2k) それぞれの特徴を簡単に見ていきます。 LuigiとApache AirflowはPythonでワークフローを記述します。これらは機械学習での利用が多いのですが、より一般的なユースケースにも利用可能です。Pythonによるツール独自の記法も含むため慣れていない場合は一定の学習が必要になります。 Tekton PipelinesとArgo WorkflowsはKubernetesネイティブなワークフローエンジンであり、Kubernetesマニフェストを使ってワークフローを記述します。Kubernetesの利用が前提となるため、運用基盤が整っていない場合は導入の障壁が高いです。 Digdagはdigファイルと呼ばれる独自ファイルにワークフローを定義します。記法はYAMLに近くシンプルな表現が可能です。ZOZOでは採用しているプロダクトが多いためノウハウは蓄積されています。 Kubernetesネイティブなワークフローエンジン FAANSでは「Kubernetesネイティブなワークフローエンジン」を採用する方針にしました。FAANSはKubernetesを採用しており、エコシステムの1つとして迎え入れる基盤が既にありました。そして、後述の「Kubernetesネイティブであることの利点の大きさ」が決め手となりました。 Kubernetesネイティブとは ここでは「Kubernetesネイティブ」を「Kubernetesで動かすことを前提に設計された」と定義しています。つまりコンテナでのタスク実行を前提としたワークフローエンジンと言えます。では、Kubernetesネイティブであると何がよいのでしょうか。次のような利点があると考えます。 ワークフロー定義をマニフェストで管理できる ワークフローエンジンの移行性が高い リソース利用効率が高い それでは1点ずつ詳解していきます。 ワークフロー定義をマニフェストで管理できる ワークフロー定義をマニフェストで管理できるということは、DeploymentやIngressといった既存のKubernetesリソースと同じ方法でワークフロー定義を扱えるということです。ここでの「ワークフロー定義」は「どのようなタスクをどのような順番で実行するかの決まりごと」という抽象的な意味合いで捉えてください。 次の図はワークフローがKubernetes上で実行されるまでの流れを示しています。 【1】のステップではワークフロー定義をマニフェストに落とし込みます。【2】のステップではマニフェストをkube-apiserver経由でクラスタに登録します。そして【3】のステップで実際にPod上で処理が実行されます。実際にはカスタムリソースの作成もありますがここでは省略しています。 このようにワークフロー定義をマニフェストとして管理できることで次のメリットがあります。 GitOpsのプラクティスを適用 Control loopによる制御 データストアとしてetcdを使用 一点ずつ補足していきます。 GitOps とはGitを一元的なソースとしてKubernetesリソースを管理する手法です 3 。ワークフロー定義をマニフェストとして管理することでGitOpsのプラクティスを適用できます。これによりKubernetes上のワークフローが自動的にGitの状態と一致するようになります。 続いて、 Control loop についてです。Control loopはKubernetesにおける更新ロジックです。現実状態をマニフェストに記載された理想状態へ収束させることができます。これを実現するのがControllerです。ApplyされたワークフローはControllerによって監視されます。そして、ControllerがControl loopを回すことでワークフローが常に理想状態と一致するように更新されます。 最後にデータストアについてです。ワークフロー定義をマニフェストで管理できるということは、これらの保存のためにデータストアを別途用意する必要がありません。なぜならマニフェストは etcd に保存されるからです。etcdはKubernetesクラスタ内部のコンポーネントで、クラスタに登録される全ての情報が保存されます。別途データベースを構成するとEOLや脆弱性対応に運用負荷がかかるため、このメリットは大きいと感じます。ただし、ログの永続化にはロギングサービスやデータベースが必要となります。あくまでワークフロー定義を保存する上で別途データストアは不要ということですのでご注意ください。 以上から、ワークフロー定義をマニフェストで管理することにより、Kubernetesの既存の仕組みをパワフルに活用したワークフローの運用が可能だと分かります。 ワークフローエンジンの移行性が高い Kubernetesネイティブなワークフローエンジンではワークフロー内の各タスクはコンテナで実行されます。どのタスクをどのような順番で実行するかはマニフェストで記述されますが、タスク内でどのような処理を実行するのかという関心事はコンテナに寄せられます。これによりワークフローを利用するシーンにおいても、コンテナ一般のメリットである「可搬性」を享受できます。これには具体的に次のようなメリットがあります。 ローカルで簡単な動作検証ができる ワークフローエンジンの移行性が高い 2点目は特に強力です。タスクを独自仕様や特定言語で定義する場合と比較し、コンテナでパッケージングされたタスクは再利用可能です。つまり、コンテナをサポートする他ワークフローエンジンへの移行が比較的容易です。加えて、規模縮小によりワークフローエンジンを廃止したとしても Kubernetes Job / Kubernetes CronJob で動作させることが可能です。 リソース利用効率が高い Kubernetesネイティブなワークフローエンジンはリソース利用効率が高く、相性のよいマネージドサービスと組み合わせることでコストパフォーマンスを上げることができます。 ここでの「リソース利用効率」とは、アサインされたコンピューティングリソースに対して、実質的に利用されるリソースの割合を意味しています。ワークフローで必要となるリソースとアサインされるリソースに過不足がないほど、リソース利用効率の高い状態となります。ワークフローという処理形態では、必要なときに必要な分だけリソースを確保できればよいため、要求に応じてアサイン量を調整できることがより重要となります。 この点において、Kubernetesネイティブなワークフローエンジンは優秀です。なぜなら、タスクをPodとして切り出して実行できるからです。Kubernetesのマネージドサービスが提供する機能を利用すれば、Podの需要に合わせてNodeをスケールできます 4 。さらに、Pod単位でスケール可能なサービス 5 を利用すればさらに効率が高まります。このように、Kubernetesネイティブなワークフローエンジンをクラウド環境のマネージドサービスで動作させることにより、リソース利用効率ひいてはコストパフォーマンスを高めることができます。 Argo Workflowsの選定理由 以上から、選定候補として残ったのがKubernetesネイティブなワークフローエンジンであるTekton PipelinesとArgo Workflowsの2つでした。最終的にArgo Workflowsを選択しましたが、どちらもFAANSにおける最低限の要件は満たしていました。 その上でArgo Workflowsを選んだ理由は、既に Argo CD を運用しており親和性があったからです。具体的には Argo CD Dexと連携可能 であり認証のための設定を1から構築する手間が省けるという点です。 Argo Workflowsの構成 本章ではArgo Workflowsをどのような構成で利用しているかを解説していきます。Argo Workflowsの基本的な概念からFAANSでの構成例まで説明しています。 Argo Workflowsのコアコンセプト まずはじめにArgo Workflowsの コアコセプト をご紹介します。主なカスタムリソースとして Workflow 、 WorkflowTemplate 、 CronWorkflow があります。それぞれの概要は下表の通りです。 カスタムリソース 説明 Workflow 最も基本的なリソース。ワークフローの定義と実行ステータスを持つ。 WorkflowTemplate 頻繁に利用するワークフローをテンプレートとして定義するリソース。 CronWorkflow スケジュール実行したいワークフローを定義するリソース。 全体構成 全体観を掴んでいただくためにマニフェスト作成からワークフロー実行までの構成を下図に示します。なお、Kubernetes環境には GKE Autopilot を利用しています。 ワークフロー実行の流れ 基本的な流れは前述の通りです。ワークフロー定義を落とし込んだマニフェストをKubernetesクラスタに登録し、Podでタスクが実行されます。ワークフローのデプロイにはArgo CDを用いています。Argo CDはGitOpsを実現するCI/CDツールです。Argo Workflowsにおけるワークフロー定義はマニフェストとして作成されるため、GitOpsのプラクティスを適用できます。これによりKubernetesの実状態をGitHubのソースへ自動的に追従させることができます。 マニフェストの分散管理 アプリケーションリポジトリを監視する Argo CD Application の存在に違和感を感じた方もいらっしゃるかと思います。FAANSでは従来、Kubernetesのマニフェストは全てインフラリポジトリで管理していました。しかし、開発チームが運用するワークフローに限り、それらのマニフェストをアプリケーションリポジトリに切り出して管理しています。これには開発サイドの裁量でワークフローを管理できるため開発効率がよいという利点があります。 Twelve-Factor App にある コードベース の思想から逸脱しているようにも見えますが、現時点で大きな課題は生じておらずメリットの方が大きいと感じています。 Workflowの構成 FAANSでのWorkflowの構成例をご紹介します。主に利用するカスタムリソースは WorkflowTemplate と CronWorkflow の2つです。 WorkflowTemplate に再利用可能なワークフローをまとめておき、それを CronWorkflow から呼び出すという構成を基本としています。これによりマニフェストの責任範囲が明確になり運用管理がしやすくなります。 文章だけでは分かりにくいのでサンプルマニフェストを基に説明していきます。 下記の WorkflowTemplate は 公式サンプル をFAANSでの利用形態に近づけたものです。 templates フィールドは大きく2つのテンプレート種別に分かれています。便宜上、本稿では部品テンプレートと製品テンプレートと呼称します。この例では、 whalesay-template が部品テンプレート、 main と main-on-monday が製品テンプレートになります。製品テンプレートは部品テンプレートを1以上利用して目的の処理を実現します。このような構成にすることでテンプレートをDRYに保ちます。 apiVersion : argoproj.io/v1alpha1 kind : WorkflowTemplate metadata : name : workflow-template spec : templates : - name : whalesay-template # 部品テンプレート inputs : parameters : - name : message container : image : docker/whalesay command : [ cowsay ] args : [ "{{inputs.parameters.message}}" ] - name : main # 製品テンプレート dag : tasks : - name : inner-A template : whalesay-template # 部品テンプレートを使用 arguments : parameters : - name : message value : hello world # パラメータ注入 - name : inner-B dependencies : [ inner-A ] template : whalesay-template arguments : parameters : - name : message value : hello again - name : main-on-monday # 製品テンプレート dag : tasks : - name : inner-A template : whalesay-template arguments : parameters : - name : message value : hello monday 続いて、 CronWorkflow のサンプルを見ていきます。こちらは上の WorkflowTemplate の製品テンプレートである main-on-monday を呼び出し、月曜日のみ実行されるスケジュールを組んでいます。 CronWorkflow には、どの製品テンプレートをいつ呼び出すのか、履歴をどのくらい保持するのかといった関心だけを詰め込むようにしています。 apiVersion : argoproj.io/v1alpha1 kind : CronWorkflow metadata : name : cron-workflow-on-monday spec : schedule : "0 6 * * 1" # 毎週月曜午前6時に実行 timezone : "Asia/Tokyo" startingDeadlineSeconds : 0 concurrencyPolicy : "Allow" successfulJobsHistoryLimit : 4 failedJobsHistoryLimit : 4 suspend : false workflowSpec : entrypoint : main templates : - name : main steps : - - name : run templateRef : name : workflow-template template : main-on-monday # 製品テンプレートを呼び出し 以上のような構成にすることで、 WorkflowTemplate と CronWorkflow の責務の違いが明確になり、開発や運用がしやすくなります。なお、ワークフローの内容や数量によって適切な構成は異なりますのでご留意ください。 Webコンソールの紹介 最後にArgo WorkflowsのWebコンソールをご紹介します。 ログイン画面 Webコンソールにアクセスするとログイン画面が表示され、一番左のログインボタンを押下するとSSOでログインできます。SSOを実現するために Argo CD Dexとの連携 および Azure ADでのSAML認証 を利用しています。また独自ドメインによるHTTPSでのアクセスを GoogleマネージドSSL証明書 で可能にしています。 Workflow一覧 実行された Workflow の一覧を確認できます。表示する件数 6 や保持期間 7 を、成功・失敗の場合に分けて柔軟に設定できます。 Workflow詳細 一覧画面から項目を押下すると実行結果の詳細を確認できます。タスク間の依存関係と成否がグラフィカルに表示されます。 失敗すると次のような表示になります。タスクを押下すると詳細が表示され、実行後のPodを保持していれば MAIN LOGS よりログを確認できます。なお最後尾の成功しているタスクはSlack通知です。 WorkflowTemplate一覧 クラスタに登録された WorkflowTemplate の一覧を確認できます。 WorkflowTemplate詳細 一覧画面から任意の項目を押下すると詳細を確認できます。 SUBMIT ボタンを押下することでWebコンソールからもWorkflowの実行が可能です。マニフェストの編集も可能であるため検証に役立ちます。 CronWorkflow一覧 クラスタに登録された CronWorkflow の一覧を確認できます。 WorkflowTemplate と異なり SCHEDULE や NEXT RUN が表示されています。 CronWorkflow詳細 一覧画面から任意の項目を押下すると詳細を確認できます。 WorkflowTemplate 同様に手動による実行や、マニフェストの編集が可能です。 まとめ Kubernetesネイティブなワークフローエンジンの特徴とFAANSにおけるArgo Workflowsの構成をご紹介しました。ワークフロー定義をマニフェストで管理できる点は非常に強力ですし、移行性やリソース利用効率の高さも魅力的です。既にクラウド環境でKubernetesを運用しており、かつ、ワークフローエンジンの導入を検討されている方にとっては有力な候補となります。本稿が検討の一助となれば幸いです。 おわりに 最後に、ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com ここでの「ログの確認」とはPod内の永続化されていないログをWebコンソールから参照することを指します。 ↩ 他にも Kubeflow Pipelines や Prefect などあります。 ↩ GitOpsの詳細は CircleCI社のブログ が参考になりますのでご参照ください。 ↩ 例えばGoogle Cloud Platformであれば cluster autoscaler や node auto-provisioning が該当します。 ↩ Pod単位でスケール可能なサービスには Amazon EKS on AWS Fargate や GKE Autopilot があります。 ↩ CronWorkflowでは オプション を使ってWorkflowの保持件数を設定できます。 ↩ ttlStrategy を利用してWorkflow毎に保持期間を設定できます。 ↩
はじめに こんにちは。SRE部ECプラットフォーム基盤SREブロックの大澤と立花です。 本記事ではマイクロサービスのカナリアリリースに関して私達が抱えていた課題と、それをFlaggerによるプログレッシブデリバリー導入でどのように改善したのかを紹介します。 ZOZOTOWNのマイクロサービス基盤におけるカナリアリリース手段の変遷については以下のテックブログで紹介しておりますので気になった方はご参照ください。現在はIstio VirtualServiceの加重ルーティングを用いたカナリアリリースに一本化しております。 techblog.zozo.com techblog.zozo.com 目次 はじめに 目次 カナリアリリースの運用課題 解決手段としてのプログレッシブデリバリー Flaggerとは? Flaggerによるプログレッシブデリバリーの進み方 Flagger導入時の検討ポイント (1) サービスを瞬断させることなくFlaggerを導入する (2) Datadogのメトリクスを利用してカナリア分析する (3) リリース開始・完了をSlackに通知する プログレッシブデリバリー導入の効果 (1) リリース対応稼働の削減 (2) リリースミスによる障害範囲の最小化 (3) カナリア用リソースのコスト削減 最後に カナリアリリースの運用課題 カナリアリリースすることで、新しいアプリケーションにバグがあった場合でもユーザへの影響を最小限に抑えられます。一方で、通常のリリースと比較して運用負荷が大きくなるデメリットも存在します。 こちらの図はカナリアリリースの進行を示したものです。 以下を繰り返すことでカナリア用Pod(以下、Canary Podとする)への加重を増やしていきます。 n%デプロイ Canary Podのreplicas拡張・加重増加の設定変更 分析 Canary Podに流れたリクエストが問題なく処理されているか、レイテンシが悪化していないか判断 問題がない場合、n%デプロイを進行する 問題があった場合、旧バージョンにロールバックする 従来のカナリアリリースでは、これらのサイクルを人が担うことになります。Canary Podへの加重変更プルリクエスト作成、レビュー、CI/CDの待ち時間、エラー発生やレイテンシ悪化の監視といったことが加重変更の都度発生します。このため、カナリアリリースだけでリリース担当者の稼働を半日費やしてしまうことも珍しくなく負担が非常に大きい状態でした。 また、私達のチームではメインのリソースとカナリアのリソースでKubernetesのDeploymentのマニフェストを分けて管理していました。これら2つのマニフェストを日頃からメンテナンスすることになりますので、変更やレビュー対象の増加に繋がり、設定漏れといった人的ミスのリスク要因になりがちです。 解決手段としてのプログレッシブデリバリー 私達のチームが管理する検索APIはリリース頻度が比較的高く、カナリアリリースによる時間的拘束が顕著でしたので改善を目指すことにしました。 そこで挙がったのがプログレッシブデリバリーです。 プログレッシブデリバリーはリリースプロセスを自動化するために、メトリクスの取得・分析を行い、問題がなければデプロイの進行、基準を満たさなければロールバック、といった判断と実行を備えた仕組みです。 このプログレッシブデリバリー用ツールには、Argo Rollouts、Flagger、Spinnakerなどがありますが、私達のチームではFlaggerを採用することにしました。 Flaggerとは? Flagger とはプログレッシブデリバリーを実現するKubernetes Operatorです。 私達のマイクロサービス基盤で採用しているツール(Istio, Datadog, Slack)の場合ですと、Flaggerはカナリアリリースを自動で進めるために次のように連携します。 加重変更 : Istio VirtualServiceの設定変更 スケールアウト/イン : DeploymentをオートスケールするためのHorizontalPodAutoscalerの設定変更 メトリクス取得 : Datadogにクエリ発行 通知・アラート : Slackに通知 連携可能なツールについては 公式ドキュメントのIntroduction で紹介されております。 用途 連携可能なツール トラフィックルーティング Istio, App Mesh, Linkerd, Kuma, Open Service Mesh, Contour, Gloo, nginx, Skipper, Traefik リリース分析のためのメトリクス取得 Datadog, Prometheus, InfluxDB, New Relic, CloudWatch, Stackdriver, Graphite 通知、アラート Slack, MS Teams, Discord, Rocket Flaggerを採用した理由については以下の通りです。 Istioとの連携がサポートされていること FlaggerはIstioを活用したプログレッシブデリバリーをサポートしており、自動でVirtualServiceの加重を変更できます。マイクロサービス基盤はIstio導入済みで、カナリアリリースの際には元々手動でVirtualServiceを変更していました。加重変更の手段は今までと変わらないため、導入イメージが湧きやすいことは重要なポイントでした。 Datadog、Slackとの連携がサポートされていること FlaggerはDatadogのメトリクス取得をサポートしています。私達のチームでは監視は主にDatadogを利用しており、カナリアリリースの進行・ロールバック判断もDatadogを活用していました。人が目視でチェックしていたグラフのメトリクスをFlaggerからも同じように利用できることは重要なポイントでした。 モダンなツールであれば通知手段としてSlackが提供されていることは一般的かもしれませんが、リリースの成否通知をSlack連携できることは必須要件でした。 Flaggerによるプログレッシブデリバリーの進み方 Flaggerがどのようにプログレッシブデリバリーを進めていくかをご説明します。便宜上、旧バージョンをv1、新バージョンをv2として記載します。またわかりやすくするために加重変更は100%まで行う設定とし、Serviceなどのリソースは記載を省略しています。 まずリリース前の状態です。v1のPrimary Podのみが存在し、勿論加重はPrimary Podに100%となっております。 対象マイクロサービスのDeploymentが更新されてrevisionが上がったことをFlaggerが検知するとプログレッシブデリバリーが開始され、 Progressingフェーズ に入ります。このフェーズではまずCanary Podがv2としてPrimary Podと同じスケールで生成されます。v2のCanary Podが生成された後、メトリクスを取得し分析しながらCanary Podへの加重を徐々に増やしていきます。 加重切り替えが順調に進みCanary Podへの加重が100%になると、 Promotingフェーズ に入ります。このフェーズではPrimary Podがv2へアップデートされます。アップデート完了後、加重をCanary PodからPrimary Podに徐々に戻していきます。 加重が完全にPrimary Podに戻ると、 Finalisingフェーズ に入ります。このフェーズでは不要になったCanary Podを停止するためにスケールが0に変更されます。 Canary Podのスケールが0になると、 Succeededフェーズ になりプログレッシブデリバリーが完了となります。 Flagger導入時の検討ポイント Flaggerを検索APIに導入するにあたり、基本機能の検証や日々のリリース業務に落とし込めるか検討しました。その中でも円滑な導入・運用のためにSREチームとして特に注力した3点について紹介します。 (1) サービスを瞬断させることなくFlaggerを導入する 最初にIstioサービスメッシュ内でのFlaggerの挙動を説明します。sample-appというDeploymentが存在する状態でカスタムリソースであるCanaryを適用すると、Flaggerは次の3つの動きをします。 Flaggerは app ラベルが sample-app と一致するDeployment(またはDaemonSet)をCanary適用のターゲットにする。ターゲットになった既存Deploymentはreplicasが0へ更新される。 ターゲットの既存Deploymentを元に新しいDeploymentが作成される。このDeploymentは name と app ラベルが sample-app-primary になる。 Flaggerによるプログレッシブデリバリーの進み方 で説明したPrimary Podはここで作成されたDeploymentが対応している。 新しいDeployment用のService、VirtualService、DestinationRule、HorizontalPodAutoscalerがCanary定義に従い新規に作成される。 詳細な挙動については 公式ドキュメントのHow it works をご覧ください。 ここでCanaryを稼働中の検索APIへ適用する障害となったのが、Flaggerがターケットとするラベル名でした。Flaggerはデフォルトでは app ラベルを使用します。そして、Flagger導入前の検索APIは以下のような通信経路を構築していました。 ここで重要なのは、ServiceとDeploymentの紐付けに app ラベルを使用していた点です。この状態の検索APIへCanaryを適用すると次の問題が発生します。 既存Deploymentはreplicasが0へ更新されているため通信できない。 新たに作成されたDeploymentは app ラベルが zozo-search-api-primary へ更新されているため、既存Serviceの app ラベルとの不一致で通信できない。 このようなCanary適用後の振る舞いにより、デフォルト設定のままFlaggerを導入すると検索APIと通信できなくなる事象が発覚しました。 通信経路を確保しつつFlaggerを導入するためには、新たに作成されるDeploymentの app ラベルは更新を避ける必要があります。幸いなことにFlaggerはCanary適用のターゲットラベルを変更するオプションを提供しています。下記は検索APIにおいて自動的にラベルの書き換えが行われても影響を受けない run へ変更した時のコード事例です。 selector-labels を設定することでターゲットになるラベルを変更できます。 - name: flagger image: ghcr.io/fluxcd/flagger:1.11.0 imagePullPolicy: IfNotPresent args: - -log-level=info - -include-label-prefix=app.kubernetes.io - -mesh-provider=istio - -selector-labels=run 既存Serivceは app ラベルを元に通信する。 新たに作成されたServiceは run ラベルを元に通信する。 このように既存の通信経路を確保する方法でFlagger導入を進めました。安定稼働していることを確認した後、検索APIを呼び出す経路を新たな通信経路へと切り替えFlagger導入は完了となりました。 (2) Datadogのメトリクスを利用してカナリア分析する FlaggerはカスタムリソースMetricTemplateを使用することで、Datadogなど様々な外部サービスをカナリア分析に利用できます。加えてMetricTemplateは、Datadogのメトリクスを作成するクエリをそのまま利用できます。 以下は99パーセンタイルのレイテンシが1sを超えたことを閾値とする場合の設定例です。 apiVersion: flagger.app/v1beta1 kind: MetricTemplate metadata: name: zozo-search-api-99tile-latency spec: provider: type: datadog address: https://api.datadoghq.com secretRef: name: zozo-search-api-datadog-secrets-20211228101017 query: | p99:trace.servlet.request{env:prd,service:zozo-search-api} このテンプレートをCanaryのanalysisから参照します。 analysis: metrics: - name: "99tile-latency" templateRef: name: zozo-search-api-99tile-latency thresholdRange: max: 1 このように、クエリを自由に記述できるため様々な指標をカナリア分析に利用できます。 (3) リリース開始・完了をSlackに通知する プログレッシブデリバリーにより自動でカナリア分析可能となったら、人間がカナリアリリース全体の状況を注視してる時間も可能な限り削減したいと考えます。そこでSREチームではFlaggerのイベントをSlackに通知し、カナリアリリースの開始・終了を検知しています。 プログレッシブデリバリー導入の効果 プログレッシブデリバリー導入により得られた効果を3点紹介します。 (1) リリース対応稼働の削減 冒頭でカナリアリリースの辛みとして記載した以下の対応を、プルリクエスト1つでFlaggerが実行するようになりました。 n%デプロイ Canary Podのreplicas拡張・加重増加の設定変更 分析 Canary Podに流れたリクエストが問題なく処理されているか、レイテンシが悪化していないか判断 問題がない場合、n%デプロイを進行する 問題があった場合、旧バージョンにロールバックする Flaggerはn%デプロイ進行と同時に分析も実施し、エラー増加やレイテンシ悪化が発生していれば自動でロールバックを行ってくれます。 分析時のメトリクス取得は、これまで監視に利用していたDatadogダッシュボードのクエリを移植しております。これにより元々人間が判断していたのと同じロジックでFlaggerが判断できますので、リリース中の監視もFlaggerに任せています。別作業しながらSlackの成否通知を待っていれば良いため作業時間の捻出にも寄与できております。 また、2022年4月に実施したElasticsearchのアップグレードに伴うクラスタ切り替え時には、Elasticsearchの負荷を監視しながら加重を少しづつ上げたいニーズがありました。段階的な加重増加をFlaggerに任せられたおかげで、私達は負荷の監視に注力できました。 (2) リリースミスによる障害範囲の最小化 検索APIリリース時に、とある理由で新アプリケーションにエラーが発生してしまったことがありました。これは当時の検索APIのリクエスト・エラー数のグラフとSlackの様子になります。 カナリアリリース開始後10%リリースの時点で、Flaggerがメトリクス分析にてエラーを検知し自動でロールバックしてくれたため障害影響を最小限に抑えられました。 (3) カナリア用リソースのコスト削減 Flagger導入前は、カナリアリリースのためにリリース時以外でもCanary Podを最低1つは起動しておく必要がありました。オートスケーリングに利用しているHorizontalPodAutoscalerの仕様上、minReplicasを0にできないためです。 Flagger導入後は、Canary Podが Progressingフェーズ から Finalisingフェーズ の間しか起動しないため、コスト削減にも寄与できております。 また、カナリア用のマニフェストファイルも不要になりましたので、構成管理としてもDRYな状態を保てるようになりました。 最後に Flaggerによるプログレッシブデリバリーを導入しカナリアリリースを自動化したことで、リリース作業の削減・障害範囲の最小化・コスト削減といった多くのメリットを享受できました。 ただし、これまでカナリアリリース不要と判断していた軽微な修正でも必ずカナリアリリースになるため、リリース時間が延びてしまうケースもありました。 1 しかし、万が一の障害発生時に障害範囲を最小限にし自動ロールバックも備えた仕組みがあることは、リリースに対する心理的なハードルを下げ開発サイクルを高速化することに繋がっていると感じております。 引き続き、快適な開発・運用を実現すべく改善していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co skipAnalysis をtrueにすることで通常のリリースに変更することも可能ですが、開発環境以外では変更しないようにしております。 ↩
はじめに こんにちは。ECプラットフォーム部の北原です。普段はZOZOTOWNのバックエンドの開発、運用に携わっており、現在は会員機能を司るマイクロサービスの開発を進めています。 今回はZOZOTOWNのGo言語におけるマイクロサービス開発の共通規約を守るための取り組みを紹介します。 マイクロサービス開発の課題 ZOZOTOWNでは複数のマイクロサービスでGo言語を使っています。マイクロサービスではトレース、ヘッダー処理、認証関連などの機能をサービス毎に持つことはよくあると思います。一方で、マイクロサービス開発ではサービス毎に別のチームが開発することもよくあるため、実装者による認識の齟齬、漏れなどで同一機能の実装に差異が生じてしまうかもしれないという課題があります。 開発の当初から共通機能の管理への課題感はあり、ZOZOTOWNのGo言語におけるマイクロサービス開発の共通規約を守るため、標準的な機能を盛り込んだ開発テンプレートを作り課題に取り組んでいます。 開発テンプレート 開発テンプレートの共通規約の実装、共通機能の例、構成を紹介します。 共通規約の実装例 開発テンプレートはバックエンドの共通規約の実装、共通ライブラリ、ドメインロジックのサンプルコードを開発プロジェクトとして展開できるようにしたものとなります。 バックエンドの共通規約の実装例として次のようなものがあります。 トレース ヘッダー処理 認証 それぞれ紹介していきます。 トレース マイクロサービスのログやトレースではいくつかのサービスを使っており次のような住み分けをしています。 機能・サービス 用途 アクセスログ 障害調査やユーザからの問い合わせ対応 Datadog トレース分析、アラート検知 Sentry 未知のエラー検知、エラーの管理 例として、アクセスログの実装を抜粋したものとなります。まず、アクセス時にロガーを呼び出すミドルウェア(HTTPハンドラにおけるミドルウェアパターン)を、次にロガーの実装を示します。共通の項目を定義しサービス毎にズレがないよう共通としています。 TraceID を持っていますが、Datadogでは全てのトレースを保持できないため、Datadogで破棄されたトレースの調査用に保持しています。 アクセスログコード抜粋 func (mw loggingMiddlewareImpl) LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { accessLog := &httpLogger.AccessLog{ Method: r.Method, Host: r.Host, Path: r.URL.Path, Query: r.URL.RawQuery, RequestSize: r.ContentLength, UserAgent: header.GetUserAgent(r), ... TraceID: r.Header.Get(constant.HeaderKeyZozoTraceID), UID: r.Header.Get(constant.HeaderKeyZozoUID), } ctx := setAccessLog(r.Context(), accessLog) sw := &StatusResponseWriter{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(sw, r.WithContext(ctx)) accessLog.Status = sw.status requestedAt, err := GetRequestedAt(ctx) if err != nil { lib.LogError(ctx, err) } else { accessLog.Latency = time.Since(requestedAt).Seconds() } mw.accessLogger.Log(accessLog) }) } type loggingMiddlewareImpl struct { accessLogger httpLogger.AccessLogger } httpLogger.AccessLogger では現在は zap.Logger を利用しています。ユーザからの問い合わせ調査など、サービスを横断した調査を円滑にするためサービス間で出力される情報が揃っていることが求められます。フォーマットや日時の精度など、機能が提供されることでサービス横断的な品質担保が容易になります。 ロガーコード抜粋 type AccessLogger interface { Log(accessLog *AccessLog) } type accessLoggerImpl struct { *zap.Logger } func (l accessLoggerImpl) Log(a *AccessLog) { l.Info( zap.Int( "status" , a.Status), zap.String( "method" , a.Method), zap.String( "host" , a.Host), zap.String( "path" , a.Path), ... ) } ヘッダー処理 APIではエンドポイントに送る値としてユーザの入力パラメータでない値はヘッダーでやりとりすることが多いと思われます。User Agentなどのユーザ情報や、認証情報、APIの呼び出し元の情報など必要なヘッダー情報はマイクロサービス間でも持ち回る共通規約となっています。 APIの処理中で別のマイクロサービスのAPIを呼び出す際も同等のヘッダーを付加する必要があるためContextで持ち回っています。これもマイクロサービスとして共通の実装にすることで抜け漏れのない機能として提供できます。 ヘッダー処理のミドルウェアコード抜粋 func RequestMiddleware(next http.Handler) http.Handler { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { ctx := r.Context() if userAgent := r.Header.Get(constant.HeaderKeyForwardedUserAgent); userAgent != "" { ctx = setForwardedUserAgent(ctx, userAgent) } if userIP := r.Header.Get(constant.HeaderKeyUserIP); userIP != "" { ctx = setUserIPAddress(ctx, userIP) } if traceID := r.Header.Get(constant.HeaderKeyZozoTraceID); traceID != "" { ctx = SetTraceID(ctx, traceID) } if uid := r.Header.Get(constant.HeaderKeyZozoUID); uid != "" { ctx = setUID(ctx, uid) } if xForwardedFor := r.Header.Get(constant.HeaderKeyXForwardedFor); xForwardedFor != "" { ctx = setXForwardedFor(ctx, xForwardedFor) } if r.RemoteAddr != "" { ctx = setRemoteAddress(ctx, r.RemoteAddr) } next.ServeHTTP(w, r.WithContext(ctx)) }) } 別のマイクロサービスのAPI呼び出しHTTPクライアントのヘッダー追加コード抜粋 func setHeaders(ctx context.Context, request *http.Request) error { if encodedInternalIDToken, err := middleware.GetEncodedInternalIDToken(ctx); err == nil { request.Header.Set(constant.HeaderKeyZozoInternalIDToken, encodedInternalIDToken) } if ipAddress, err := middleware.GetUserIPAddress(ctx); err == nil { request.Header.Set(constant.HeaderKeyUserIP, ipAddress) } if userAgent, err := middleware.GetForwardedUserAgent(ctx); err == nil { request.Header.Set(constant.HeaderKeyForwardedUserAgent, userAgent) } if traceID, err := middleware.GetTraceID(ctx); err == nil { request.Header.Set(constant.HeaderKeyZozoTraceID, traceID) } if uid, err := middleware.GetUID(ctx); err == nil { request.Header.Set(constant.HeaderKeyZozoUID, uid) } if apiClient, err := middleware.GetAPIClient(ctx); err == nil { request.Header.Set(constant.HeaderKeyAPIClient, apiClient) } if xForwardedFor, err := middleware.GetXForwardedFor(ctx); err == nil { if remoteAddr, err := middleware.GetRemoteAddress(ctx); err == nil { host, _, e := net.SplitHostPort(remoteAddr) if e != nil { return xerrors.Errorf( "split remote address: %v" , e) } request.Header.Set(constant.HeaderKeyXForwardedFor, xForwardedFor+ ", " +host) } } return nil } 実装自体は非常にシンプルなものですが、シンプルであっても個別に実装せずヘッダーを伝播させる規約を守るためコピー用のコードを用意しています。 認証 認証はバックエンドの前段にあるAPI Gatewayで行われており、認証が成功した場合にはバックエンドへの通信のヘッダーに認証されたことを表すトークンが付与されます。 ヘッダーで渡されるトークンからユーザの情報を取得するためデコード処理を行っていますが、デコードの仕様や取得した値のバリデーションなど実装の差異がないよう共通処理としています。 認証ユーザ情報取得コード抜粋 func InternalIDTokenMiddleware(next http.Handler) http.Handler { return http.HandlerFunc( func (w http.ResponseWriter, r *http.Request) { token := r.Header.Get(constant.HeaderKeyZozoInternalIDToken) internalIDToken, err := model.DecodeInternalIDToken(token) if err != nil { writeRespInvalidInternalIDToken(w) return } ctx := setInternalIDToken(r.Context(), internalIDToken) ... next.ServeHTTP(w, r.WithContext(ctx)) }) } API Gatewayで認証されたユーザ情報をDecodeInternalIDTokenで復元しContextにセットします。ミドルウェアのパッケージ外で値の変更ができないようレイヤーに閉じた実装として提供しています。 共通機能の例 バックエンドやAPIとしてよく利用され共通化できる機能もテンプレートとして提供しています。 ヘルスチェックのように各サービスで実装する機能は重複実装しないようテンプレートに含めています。 ヘルスチェックコード抜粋 type clientOptions struct { healthCheckReadiness func () error } func NewHealthCheckControllerWithReadiness(readiness func () error ) func (*http.Request, RequestParameters) ([] byte , error ) { options.healthCheckReadiness = readiness return healthCheckController } func healthCheckController(r *http.Request, _ RequestParameters) ([] byte , error ) { switch r.Method { case http.MethodGet: switch r.URL.String() { case "/health/liveness" : return healthCheckLiveness() case "/health/readiness" : return healthCheckReadiness() default : return nil , view.ErrNotFound } default : return nil , view.ErrNotFound } } func healthCheckLiveness() ([] byte , error ) { return [] byte ( "{}" ), nil } func healthCheckReadiness() ([] byte , error ) { if err := options.healthCheckReadiness(); err != nil { return nil , view.ErrServiceUnavailable } return [] byte ( "{}" ), nil } HTTPサーバーのrouter設定コード抜粋 func init{ ... opt := controller.ClientOptions{HealthCheckReadiness: mysql.Readiness} healthCheckController := controller.NewHealthCheckControllerWithReadiness(opt) routes := map [ string ] func (*http.Request, controller.RequestParameters) ([] byte , error ){ "/health/liveness" : healthCheckController, "/health/readiness" : healthCheckController, } for route, controller := range routes { Router.Handle(route, buildHandler(controller)) } ... } MySQLヘルスチェックコード抜粋 func Readiness() error { db, err := mysql.NewDB() if err != nil { return err } defer db.Close() err = db.Ping() if err != nil { return err } return nil } マイクロサービス作成の度に再実装するコストをかけないようにバックエンドの共通規約を守る機能やプロジェクトでよく利用される機能群をテンプレートにまとめています。 開発テンプレートの構成 テンプレートの責務はバックエンドの共通規約を守る、再利用性の向上、開発の立ち上げスピード向上にあると考えています。 要素としては、業務共通的なミドルウェア、共通ライブラリ、ドメインロジックのサンプルコードとなります。 ヘキサゴナルアーキテクチャをベースに、アダプター・アプリケーション・ドメインのレイヤーで責務を分離するようなアーキテクチャを設計しました。 アダプターレイヤーで外部依存の機能を実装し、ドメインレイヤーに業務のルールを整理、アプリケーションレイヤーでドメインを利用したロジックを実装するようなシンプルな切り分けになっています。 社内のGo言語のプロジェクトは何かしらのレイヤードアーキテクチャを採用していることが多いため、ヘキサゴナルアーキテクチャであればギャップが少なく導入しやすいと考えています。またアプリケーションレイヤーにある程度の選択肢を持たせるなどテンプレートの設計としてはマッチしていると考え選定しています。 起動するとデフォルトでHTTPサーバーが立ち上がり、ある程度必要なものが用意されているため、各ドメインロジックの実装に注力できます。 その他の技術要素 他にはlinterやテストのヘルパー類、Docker、DatadogとSentryなどテンプレートを利用することでバックエンドとしての標準機能や品質のベースが整うような構成となっています。 DockerやDatadog、SentryなどSREチームによって標準化されているものは、テンプレートとして利用しやすいようヘルパーなどを用意する形になっています。 ライブラリやディレクトリ構成を合わせることで、別のプロジェクトを参照する際の認知負荷を下げられることを期待しています。 開発テンプレートの課題とSDK化 テンプレートでの開発を進める中で運用課題となりそうなポイントが出てきました。 当初はサービス毎にテンプレートのリポジトリをコピーする方式でしたが、プロジェクト開始時のバージョンのコードがコピーされ、テンプレートの変更をサービス側で取り込む運用を想定していました。 しかし利用プロジェクトが増えるとそれぞれに反映してもらう手間、実装タイミング、プロジェクト毎に反映の有無が別れるなど運用の手間(負荷)が懸念されました。 そこでライブラリとして必要なパーツを利用できるようSDK化を進めることにしました。 SDK (Software Development Kit) として、テンプレートの機能をライブラリとして切り出しリポジトリをインポートして利用できる形にしました。 変更がインポートできるようになることで、テンプレートで懸念されたアップデートの運用の手間や取り込みのタイミングなどある程度払拭できるようになると考えました。 SDK化することで機能のつながりがわかりづらくなったり、インポートの手間が増えたりと障壁がゼロなわけではありません。 そこで、テンプレートの位置付けをSDKの利用方法を示したサンプルアプリケーションと改めることでSDKの理解が進むようにし、各サービスに展開しやすくしています。 プライベートリポジトリの利用 SDKとして切り出されたライブラリはGitHubのプライベートリポジトリで管理されています。プライベートリポジトリのアクセスはいくつか方法がありますが、インポートする際の設定で検討した内容となります。 Go言語でのプライベートリポジトリの取得に関してはこちらを参照してください。 https://go.dev/ref/mod#private-modules プライベートリポジトリのモジュールを go.mod に追加する場合は GOPRIVATE にリポジトリを指定し go get します。 GOPRIVATE を設定することで、 GONOPROXY , GONOSUMDB の対象となります。 Direct access to private modulesより引用 The GOPROXY variable does not need to be changed in this situation. It defaults to https://proxy.golang.org,direct, which instructs the go command to attempt to download modules from https://proxy.golang.org first, then fall back to a direct connection if that proxy responds with 404 (Not Found) or 410 (Gone). The GOPRIVATE setting instructs the go command not to connect to a proxy or to the checksum database for modules starting with corp.example.com. ローカルの開発ではSSHの鍵認証を使ってGitHubのプライベートリポジトリにアクセスできる環境が整っている場合が多いと思いますが、その場合SSH経由で接続できます。 Macでの例となりますが<アカウント>、<リポジトリ>はそれぞれの環境の値が入ります。 git config --global url."ssh://git@github.com/<アカウント>".insteadOf "https://github.com/<アカウント>" go env -w GOPRIVATE=github.com/<アカウント>/<リポジトリ> go get -u github.com/<アカウント>/<リポジトリ> .gitconfig [url "ssh://git@github.com/"] insteadOf = https://github.com/ GOPRIVATE はGo言語の path.Match で一致する形式で記述できるため次のような設定もできます。 go env -w GOPRIVATE=github.com/<アカウント>/* 開発での自動テストやlintを実施するため、ローカルでDocker Composeを利用して、コンテナを起動する手順があり次のような設定ファイルを追加しています。 Dockerfile でプライベートリポジトリにアクセスする際はGitHubのpersonal access token (PAT) を利用しています。 PATを docker-compose.yml の secrets に設定し --mount=type=secret して取得しています。 Dockerfile FROM golang:1.18.0-bullseye as debugger ... RUN --mount=type=secret,id=personal_access_token git config --global url."https://$(cat /run/secrets/personal_access_token):x-oauth-basic@github.com/".insteadOf "https://github.com/" ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPRIVATE=github.com/<アカウント>/* COPY go.mod go.sum ./ RUN go mod download ... docker-compose.yml services: app: build: context: . target: debugger secrets: - personal_access_token environment: ... secrets: personal_access_token: file: personal_access_token.txt personal_access_token.txt ghp_zzz.. <personal_access_token> PATの使用は極力減らしたいと考えており課題感は残るものの、このようにプライベートリポジトリを利用できるよう設定しております。 コードの管理としてはバージョン管理もポイントとしてありますが、可能な限り後方互換性を持たせ、最新のバージョンを取得する方式を維持していきたいと考えています。 これから マイクロサービス化の開発はまだまだ残っているので、チームで課題を検討する際にSDKが1つの手段となるよう柔軟に対応していければと考えています。 またSDKを広げることで開発チームのDRYから組織的なDRYにつなげ、より価値のある課題に取り組めればと考えています。 最後に ZOZOTOWNのマイクロサービス化はまだ始まったばかりです。新たなマイクロサービスの開発も目白押しです。ご興味のある方は、以下のリンクからぜひご応募ください。お待ちしています。 corp.zozo.com hrmos.co