TECH PLAY

株式会社エニグモ

株式会社エニグモ の技術ブログ

247

こんにちは、 エニグモ でデータサイエンティストをしている堀部です。 昨年末から使い始めたdbt x BigQueryについて共有します。 BigQuery歴2年、 SQL 歴5年ほどになります。 QUALIFY句が好きです。 dbtを使い始めたきっかけ 使ってみてよかった点 前処理〜特徴量生成の例 dbt_project.yml macro models Appendix:packageの利用 まとめ dbtを使い始めたきっかけ SQL での集計は嫌いではないのですが、以下の2点で困っていることがありました。 1点目は、BigQuery特有のエラー Resources exceeded during query execution: Not enough resources for query planning - too many subqueries or query is too complex (以後、too complex エラー)です。 create temp table を使って一時的な中間テーブルを挟むことで回避してきたのですが、その都度書き換えるコストがかかっていました。 2点目は、似たようなクエリを以前に書いたこと覚えがあっても、過去の自分が書いたクエリ *1 が長く該当箇所を見つけるのに時間がかかってしまうという課題がありました。 この2点をまとめて解決できそうだと感じ、使い始めたのがdbtでした。 使ってみてよかった点 with句を分割して管理できる → 部分的なクエリの再利用がしやすくなった データモデルの種類(view、table、intermediate、ephemeral)を簡単に変更できる → too complexエラーの回避が簡単に yaml (dbt_project.yml)で複数のクエリで共通に利用できる変数(vars)を管理することができ、 CLI で変数を上書きして実行することができる → 汎用的なクエリを作成して、varsだけを変更することで様々なパターンを試せるようになった また、jinja2を使ったmacroを前処理〜特徴量生成で利用してみたら便利だったので紹介します。 前処理〜特徴量生成の例 bigqueryの公開データの bigquery-public-data.ml_datasets.census_adult_income を使って実際に利用したファイルを元に紹介します。 *2 dbt_project.yml modelsとvarsの部分のみを変更しています。 # Name your project! Project names should contain only lowercase characters # and underscores. A good package name should reflect your organization's # name or the intended use of these models name : 'techblog_202201' version : '1.0.0' config-version : 2 # This setting configures which "profile" dbt uses for this project. profile : 'sample' # These configurations specify where dbt should look for different types of files. # The `source-paths` config, for example, states that models in this project can be # found in the "models/" directory. You probably won't need to change these! source-paths : [ "models" ] analysis-paths : [ "analysis" ] test-paths : [ "tests" ] data-paths : [ "data" ] macro-paths : [ "macros" ] snapshot-paths : [ "snapshots" ] target-path : "target" # directory which will store compiled SQL files clean-targets : # directories to be removed by `dbt clean` - "target" - "dbt_modules" # Configuring models # Full documentation: https://docs.getdbt.com/docs/configuring-models # In this example config, we tell dbt to build all models in the example/ directory # as tables. These settings can be overridden in the individual model files # using the `{{ config(...) }}` macro. models : techblog_202201 : temp_table : +materialized : table +hours_to_expiration : 1 table : +materialized : table view : +materialized : view vars : base_table : bigquery-public-data.ml_datasets.census_adult_income index_col : id target_col : income_bracket list_agg : - avg - max - min - stddev macro macro/get_columns_list. sql BigQueryのINFORMATION_SCHEMAを利用してカラムの一覧を取得できるmacroを作成して利用しています。typesに型のリストを渡すことで、該当する型のカラムのみを取得することができます。 {% macro get_columns_list(table_name, types=None) -%} {% set columns_query %} select column_name from `{{table_name.dataset}}.INFORMATION_SCHEMA.COLUMNS` where table_schema = " {{table_name.dataset}} " and table_name = " {{table_name.name}} " {%- if types is not none %} and data_type in ( {%- for type in types %} {%- if loop . last %} " {{type}} " {%- else %} " {{type}} " , {%- endif %} {%- endfor %} ) {%- endif %} {% endset %} {% set results = run_query(columns_query) %} {% if execute %} {% set list_results = results.columns[ 0 ]. values () %} {% else %} {% set list_results = [] %} {% endif %} {{ return (list_results) }} {% endmacro %} models jinjaで書いたクエリ ↓ dbt compile で生成されたクエリ の順で紹介していきます。 models/view/row_census_adult_income. sql 元々のテー ブルデー タにindexとなるカラム id を追加しています。 select row_number() over ( order by 1 ) as {{var( " index_col " )}}, * from `{{var( " base_table " )}}` ↓ compile select row_number() over ( order by 1 ) as id, * from `bigquery- public -data.ml_datasets.census_adult_income` models/temp_table/ stg _census_adult_income. sql カテゴリ変数に対して以下の前処理を実施 空白削除 小文字化 正規化 目的変数( income_bracket ) 2値なので0,1に変換 *3 {%- set ref_table = " row_census_adult_income " %} {%- set list_numeric_columns = get_columns_list(ref(ref_table),types=[ " FLOAT64 " , " INT64 " ]) -%} {%- set list_categorical_columns = get_columns_list(ref(ref_table),types=[ " STRING " ]) -%} select {%- for col in list_numeric_columns %} {{col}}, {%- endfor %} {%- for col in list_categorical_columns %} {%- if col != var( " target_col " ) %} normalize( lower ( trim ({{col}})), NFKC) as {{col}}, {%- endif %} {%- endfor %} case when {{var( " target_col " )}} = " <=50K " then 1 else 0 end as {{var( " target_col " )}}, from {{ref(ref_table)}} ↓ compile select id, age, functional_weight, education_num, capital_gain, capital_loss, hours_per_week, normalize( lower ( trim (workclass)), NFKC) as workclass, normalize( lower ( trim (education)), NFKC) as education, normalize( lower ( trim (marital_status)), NFKC) as marital_status, normalize( lower ( trim (occupation)), NFKC) as occupation, normalize( lower ( trim (relationship)), NFKC) as relationship, normalize( lower ( trim (race)), NFKC) as race, normalize( lower ( trim (sex)), NFKC) as sex, normalize( lower ( trim (native_country)), NFKC) as native_country, case when income_bracket = " <=50K " then 1 else 0 end as income_bracket, from `buyma-analytics`.`techblog_202201_dev`.`row_census_adult_income` models/table/feature_census_adult_income. sql 量的変数 そのまま カテゴリ変数 dense_rank()で擬似的にLabel Encoding カテゴリ変数 x 量的変数 カテゴリ変数ごとに統計量(平均、最小、最大、 標準偏差 )を取得 {%- set ref_table = " stg_census_adult_income " -%} {%- set list_numeric_columns = get_columns_list(ref(ref_table),types=[ " FLOAT64 " , " INT64 " ]) -%} {%- set list_categorical_columns = get_columns_list(ref(ref_table),types=[ " STRING " ]) -%} select {%- for numeric_column in list_numeric_columns %} {%- if numeric_column != var( " target_col " ) %} {{numeric_column}}, {%- endif %} {%- endfor %} {%- for categorical_column in list_categorical_columns %} {%- set loop_index = loop .index0 + 1 %} dense_rank () over ( order by {{categorical_column}}) as {{categorical_column}}, {%- for numeric_column in list_numeric_columns %} {%- if numeric_column not in [var( " target_col " ), var( " index_col " )] %} {%- for agg in var( " list_agg " ) %} {{agg}}({{numeric_column}}) over (partition by {{categorical_column}}) as {{agg}} _ {{numeric_column}}_by_{{categorical_column}}, {%- endfor %} {%- endif %} {%- endfor %} {%- endfor %} {{var( " target_col " )}} from {{ref(ref_table)}} order by 1 ↓ compile select id, age, functional_weight, education_num, capital_gain, capital_loss, hours_per_week, dense_rank () over ( order by workclass) as workclass, avg (age) over (partition by workclass) as avg_age_by_workclass, max (age) over (partition by workclass) as max_age_by_workclass, min (age) over (partition by workclass) as min_age_by_workclass, stddev (age) over (partition by workclass) as stddev_age_by_workclass, avg (functional_weight) over (partition by workclass) as avg_functional_weight_by_workclass, max (functional_weight) over (partition by workclass) as max_functional_weight_by_workclass, min (functional_weight) over (partition by workclass) as min_functional_weight_by_workclass, stddev (functional_weight) over (partition by workclass) as stddev_functional_weight_by_workclass, avg (education_num) over (partition by workclass) as avg_education_num_by_workclass, max (education_num) over (partition by workclass) as max_education_num_by_workclass, min (education_num) over (partition by workclass) as min_education_num_by_workclass, stddev (education_num) over (partition by workclass) as stddev_education_num_by_workclass, avg (capital_gain) over (partition by workclass) as avg_capital_gain_by_workclass, max (capital_gain) over (partition by workclass) as max_capital_gain_by_workclass, min (capital_gain) over (partition by workclass) as min_capital_gain_by_workclass, stddev (capital_gain) over (partition by workclass) as stddev_capital_gain_by_workclass, avg (capital_loss) over (partition by workclass) as avg_capital_loss_by_workclass, max (capital_loss) over (partition by workclass) as max_capital_loss_by_workclass, min (capital_loss) over (partition by workclass) as min_capital_loss_by_workclass, stddev (capital_loss) over (partition by workclass) as stddev_capital_loss_by_workclass, avg (hours_per_week) over (partition by workclass) as avg_hours_per_week_by_workclass, max (hours_per_week) over (partition by workclass) as max_hours_per_week_by_workclass, min (hours_per_week) over (partition by workclass) as min_hours_per_week_by_workclass, stddev (hours_per_week) over (partition by workclass) as stddev_hours_per_week_by_workclass, dense_rank () over ( order by education) as education, avg (age) over (partition by education) as avg_age_by_education, max (age) over (partition by education) as max_age_by_education, min (age) over (partition by education) as min_age_by_education, stddev (age) over (partition by education) as stddev_age_by_education, avg (functional_weight) over (partition by education) as avg_functional_weight_by_education, max (functional_weight) over (partition by education) as max_functional_weight_by_education, min (functional_weight) over (partition by education) as min_functional_weight_by_education, stddev (functional_weight) over (partition by education) as stddev_functional_weight_by_education, avg (education_num) over (partition by education) as avg_education_num_by_education, max (education_num) over (partition by education) as max_education_num_by_education, min (education_num) over (partition by education) as min_education_num_by_education, stddev (education_num) over (partition by education) as stddev_education_num_by_education, avg (capital_gain) over (partition by education) as avg_capital_gain_by_education, max (capital_gain) over (partition by education) as max_capital_gain_by_education, min (capital_gain) over (partition by education) as min_capital_gain_by_education, stddev (capital_gain) over (partition by education) as stddev_capital_gain_by_education, avg (capital_loss) over (partition by education) as avg_capital_loss_by_education, max (capital_loss) over (partition by education) as max_capital_loss_by_education, min (capital_loss) over (partition by education) as min_capital_loss_by_education, stddev (capital_loss) over (partition by education) as stddev_capital_loss_by_education, avg (hours_per_week) over (partition by education) as avg_hours_per_week_by_education, max (hours_per_week) over (partition by education) as max_hours_per_week_by_education, min (hours_per_week) over (partition by education) as min_hours_per_week_by_education, stddev (hours_per_week) over (partition by education) as stddev_hours_per_week_by_education, dense_rank () over ( order by marital_status) as marital_status, avg (age) over (partition by marital_status) as avg_age_by_marital_status, max (age) over (partition by marital_status) as max_age_by_marital_status, min (age) over (partition by marital_status) as min_age_by_marital_status, stddev (age) over (partition by marital_status) as stddev_age_by_marital_status, avg (functional_weight) over (partition by marital_status) as avg_functional_weight_by_marital_status, max (functional_weight) over (partition by marital_status) as max_functional_weight_by_marital_status, min (functional_weight) over (partition by marital_status) as min_functional_weight_by_marital_status, stddev (functional_weight) over (partition by marital_status) as stddev_functional_weight_by_marital_status, avg (education_num) over (partition by marital_status) as avg_education_num_by_marital_status, max (education_num) over (partition by marital_status) as max_education_num_by_marital_status, min (education_num) over (partition by marital_status) as min_education_num_by_marital_status, stddev (education_num) over (partition by marital_status) as stddev_education_num_by_marital_status, avg (capital_gain) over (partition by marital_status) as avg_capital_gain_by_marital_status, max (capital_gain) over (partition by marital_status) as max_capital_gain_by_marital_status, min (capital_gain) over (partition by marital_status) as min_capital_gain_by_marital_status, stddev (capital_gain) over (partition by marital_status) as stddev_capital_gain_by_marital_status, avg (capital_loss) over (partition by marital_status) as avg_capital_loss_by_marital_status, max (capital_loss) over (partition by marital_status) as max_capital_loss_by_marital_status, min (capital_loss) over (partition by marital_status) as min_capital_loss_by_marital_status, stddev (capital_loss) over (partition by marital_status) as stddev_capital_loss_by_marital_status, avg (hours_per_week) over (partition by marital_status) as avg_hours_per_week_by_marital_status, max (hours_per_week) over (partition by marital_status) as max_hours_per_week_by_marital_status, min (hours_per_week) over (partition by marital_status) as min_hours_per_week_by_marital_status, stddev (hours_per_week) over (partition by marital_status) as stddev_hours_per_week_by_marital_status, dense_rank () over ( order by occupation) as occupation, avg (age) over (partition by occupation) as avg_age_by_occupation, max (age) over (partition by occupation) as max_age_by_occupation, min (age) over (partition by occupation) as min_age_by_occupation, stddev (age) over (partition by occupation) as stddev_age_by_occupation, avg (functional_weight) over (partition by occupation) as avg_functional_weight_by_occupation, max (functional_weight) over (partition by occupation) as max_functional_weight_by_occupation, min (functional_weight) over (partition by occupation) as min_functional_weight_by_occupation, stddev (functional_weight) over (partition by occupation) as stddev_functional_weight_by_occupation, avg (education_num) over (partition by occupation) as avg_education_num_by_occupation, max (education_num) over (partition by occupation) as max_education_num_by_occupation, min (education_num) over (partition by occupation) as min_education_num_by_occupation, stddev (education_num) over (partition by occupation) as stddev_education_num_by_occupation, avg (capital_gain) over (partition by occupation) as avg_capital_gain_by_occupation, max (capital_gain) over (partition by occupation) as max_capital_gain_by_occupation, min (capital_gain) over (partition by occupation) as min_capital_gain_by_occupation, stddev (capital_gain) over (partition by occupation) as stddev_capital_gain_by_occupation, avg (capital_loss) over (partition by occupation) as avg_capital_loss_by_occupation, max (capital_loss) over (partition by occupation) as max_capital_loss_by_occupation, min (capital_loss) over (partition by occupation) as min_capital_loss_by_occupation, stddev (capital_loss) over (partition by occupation) as stddev_capital_loss_by_occupation, avg (hours_per_week) over (partition by occupation) as avg_hours_per_week_by_occupation, max (hours_per_week) over (partition by occupation) as max_hours_per_week_by_occupation, min (hours_per_week) over (partition by occupation) as min_hours_per_week_by_occupation, stddev (hours_per_week) over (partition by occupation) as stddev_hours_per_week_by_occupation, dense_rank () over ( order by relationship) as relationship, avg (age) over (partition by relationship) as avg_age_by_relationship, max (age) over (partition by relationship) as max_age_by_relationship, min (age) over (partition by relationship) as min_age_by_relationship, stddev (age) over (partition by relationship) as stddev_age_by_relationship, avg (functional_weight) over (partition by relationship) as avg_functional_weight_by_relationship, max (functional_weight) over (partition by relationship) as max_functional_weight_by_relationship, min (functional_weight) over (partition by relationship) as min_functional_weight_by_relationship, stddev (functional_weight) over (partition by relationship) as stddev_functional_weight_by_relationship, avg (education_num) over (partition by relationship) as avg_education_num_by_relationship, max (education_num) over (partition by relationship) as max_education_num_by_relationship, min (education_num) over (partition by relationship) as min_education_num_by_relationship, stddev (education_num) over (partition by relationship) as stddev_education_num_by_relationship, avg (capital_gain) over (partition by relationship) as avg_capital_gain_by_relationship, max (capital_gain) over (partition by relationship) as max_capital_gain_by_relationship, min (capital_gain) over (partition by relationship) as min_capital_gain_by_relationship, stddev (capital_gain) over (partition by relationship) as stddev_capital_gain_by_relationship, avg (capital_loss) over (partition by relationship) as avg_capital_loss_by_relationship, max (capital_loss) over (partition by relationship) as max_capital_loss_by_relationship, min (capital_loss) over (partition by relationship) as min_capital_loss_by_relationship, stddev (capital_loss) over (partition by relationship) as stddev_capital_loss_by_relationship, avg (hours_per_week) over (partition by relationship) as avg_hours_per_week_by_relationship, max (hours_per_week) over (partition by relationship) as max_hours_per_week_by_relationship, min (hours_per_week) over (partition by relationship) as min_hours_per_week_by_relationship, stddev (hours_per_week) over (partition by relationship) as stddev_hours_per_week_by_relationship, dense_rank () over ( order by race) as race, avg (age) over (partition by race) as avg_age_by_race, max (age) over (partition by race) as max_age_by_race, min (age) over (partition by race) as min_age_by_race, stddev (age) over (partition by race) as stddev_age_by_race, avg (functional_weight) over (partition by race) as avg_functional_weight_by_race, max (functional_weight) over (partition by race) as max_functional_weight_by_race, min (functional_weight) over (partition by race) as min_functional_weight_by_race, stddev (functional_weight) over (partition by race) as stddev_functional_weight_by_race, avg (education_num) over (partition by race) as avg_education_num_by_race, max (education_num) over (partition by race) as max_education_num_by_race, min (education_num) over (partition by race) as min_education_num_by_race, stddev (education_num) over (partition by race) as stddev_education_num_by_race, avg (capital_gain) over (partition by race) as avg_capital_gain_by_race, max (capital_gain) over (partition by race) as max_capital_gain_by_race, min (capital_gain) over (partition by race) as min_capital_gain_by_race, stddev (capital_gain) over (partition by race) as stddev_capital_gain_by_race, avg (capital_loss) over (partition by race) as avg_capital_loss_by_race, max (capital_loss) over (partition by race) as max_capital_loss_by_race, min (capital_loss) over (partition by race) as min_capital_loss_by_race, stddev (capital_loss) over (partition by race) as stddev_capital_loss_by_race, avg (hours_per_week) over (partition by race) as avg_hours_per_week_by_race, max (hours_per_week) over (partition by race) as max_hours_per_week_by_race, min (hours_per_week) over (partition by race) as min_hours_per_week_by_race, stddev (hours_per_week) over (partition by race) as stddev_hours_per_week_by_race, dense_rank () over ( order by sex) as sex, avg (age) over (partition by sex) as avg_age_by_sex, max (age) over (partition by sex) as max_age_by_sex, min (age) over (partition by sex) as min_age_by_sex, stddev (age) over (partition by sex) as stddev_age_by_sex, avg (functional_weight) over (partition by sex) as avg_functional_weight_by_sex, max (functional_weight) over (partition by sex) as max_functional_weight_by_sex, min (functional_weight) over (partition by sex) as min_functional_weight_by_sex, stddev (functional_weight) over (partition by sex) as stddev_functional_weight_by_sex, avg (education_num) over (partition by sex) as avg_education_num_by_sex, max (education_num) over (partition by sex) as max_education_num_by_sex, min (education_num) over (partition by sex) as min_education_num_by_sex, stddev (education_num) over (partition by sex) as stddev_education_num_by_sex, avg (capital_gain) over (partition by sex) as avg_capital_gain_by_sex, max (capital_gain) over (partition by sex) as max_capital_gain_by_sex, min (capital_gain) over (partition by sex) as min_capital_gain_by_sex, stddev (capital_gain) over (partition by sex) as stddev_capital_gain_by_sex, avg (capital_loss) over (partition by sex) as avg_capital_loss_by_sex, max (capital_loss) over (partition by sex) as max_capital_loss_by_sex, min (capital_loss) over (partition by sex) as min_capital_loss_by_sex, stddev (capital_loss) over (partition by sex) as stddev_capital_loss_by_sex, avg (hours_per_week) over (partition by sex) as avg_hours_per_week_by_sex, max (hours_per_week) over (partition by sex) as max_hours_per_week_by_sex, min (hours_per_week) over (partition by sex) as min_hours_per_week_by_sex, stddev (hours_per_week) over (partition by sex) as stddev_hours_per_week_by_sex, dense_rank () over ( order by native_country) as native_country, avg (age) over (partition by native_country) as avg_age_by_native_country, max (age) over (partition by native_country) as max_age_by_native_country, min (age) over (partition by native_country) as min_age_by_native_country, stddev (age) over (partition by native_country) as stddev_age_by_native_country, avg (functional_weight) over (partition by native_country) as avg_functional_weight_by_native_country, max (functional_weight) over (partition by native_country) as max_functional_weight_by_native_country, min (functional_weight) over (partition by native_country) as min_functional_weight_by_native_country, stddev (functional_weight) over (partition by native_country) as stddev_functional_weight_by_native_country, avg (education_num) over (partition by native_country) as avg_education_num_by_native_country, max (education_num) over (partition by native_country) as max_education_num_by_native_country, min (education_num) over (partition by native_country) as min_education_num_by_native_country, stddev (education_num) over (partition by native_country) as stddev_education_num_by_native_country, avg (capital_gain) over (partition by native_country) as avg_capital_gain_by_native_country, max (capital_gain) over (partition by native_country) as max_capital_gain_by_native_country, min (capital_gain) over (partition by native_country) as min_capital_gain_by_native_country, stddev (capital_gain) over (partition by native_country) as stddev_capital_gain_by_native_country, avg (capital_loss) over (partition by native_country) as avg_capital_loss_by_native_country, max (capital_loss) over (partition by native_country) as max_capital_loss_by_native_country, min (capital_loss) over (partition by native_country) as min_capital_loss_by_native_country, stddev (capital_loss) over (partition by native_country) as stddev_capital_loss_by_native_country, avg (hours_per_week) over (partition by native_country) as avg_hours_per_week_by_native_country, max (hours_per_week) over (partition by native_country) as max_hours_per_week_by_native_country, min (hours_per_week) over (partition by native_country) as min_hours_per_week_by_native_country, stddev (hours_per_week) over (partition by native_country) as stddev_hours_per_week_by_native_country, income_bracket from `buyma-analytics`.`techblog_202201_dev`.`stg_census_adult_income` order by 1 このようにして、221個の特徴量を生成することができました。 Appendix:packageの利用 dbtには package というライブラリのようなものがあります。 *4 例えば、dbt-utilsには、 get_column_values というカラムのユニークな値のリストを取得することができます。one-hot encodingを行いたい場合は、下記のように書くことができます。 *5 {%- set categorical_column = " sex " -%} {%- set ref_table = " stg_census_adult_income " -%} {%- set unique_values = dbt_utils.get_column_values(ref(ref_table), categorical_column) -%} select {%- for value in unique_values -%} case when {{categorical_column}} = " {{value}} " then 1 else 0 end as {{categorical_column}} _ {{value}}, {%- endfor %} from {{ref(ref_table)}} ↓ compile select case when sex = " male " then 1 else 0 end as sex_male, case when sex = " female " then 1 else 0 end as sex_female, from `your-project`.`your_dataset`.`stg_census_adult_income` まとめ 個人で使っているレベルですが、 SQL をエディターで書いていた時より効率よくクエリを作成することができとても便利に感じています。今回紹介しきれなかった test や docs なども業務では活用しています。今後はBigQueryMLと組み合わせて、前処理〜モデルの学習・推論までを全てdbt x BigQueryで完結させられたらなと考えています。 *6 株式会社 エニグモ 正社員の求人一覧 hrmos.co *1 : めちゃくちゃ多段のwith句を使っていることが多いです。 *2 : dbtの環境構築方法は丁寧に紹介くださっている記事がたくさんあるので割愛します。 *3 : ここも汎用的な処理にしたかったのですが手を抜きました。 *4 : packageの導入方法については割愛します。 *5 : BigQueryの カラム名 として使えない文字列が入っているとエラーになるので要注意です。 *6 : dbt-ml というBigQueryMLを実行するpackageがあります。
こんにちは、人事総務グループの 廣島 です。 エニグモ で中途・新卒採用、採用広報などを担当しています。 エニグモ は「世界を変える、新しい流れを。」をミッションに、世界166ヶ国に900万人以上の会員を擁するソーシャルショッピングサイト「 BUYMA 」を運営しています。 今回は、エンジニア部署の部長小澤さんのインタビューをお届けします。 エンジニア組織や開発体制、 エニグモ のカルチャーなどについて伺いました。 ※この記事は Enigmo Advent Calendar 2021 の25日目の記事です。 あっというまで アドベントカレンダー も最終日です!   目次 まずは簡単に経歴や自己紹介をお願いします エンジニア組織や開発体制について 現在のエンジニアの開発組織について教えて下さい 小澤さんが入社してから今までで組織はどのように変わりましたか? データテクノロジーグループはここ数年で新しくできた組織ですが、グループ立ち上げの経緯を教えて下さい 開発体制の特徴は? 機能別の開発体制に移行した背景やその後の変化はありましたか? BUYMAの開発を行う魅力は? 開発や組織の課題について エンジニア採用で大切にしていることや、活躍するメンバーとは エンジニア採用で大切にしていることはありますか? エンジニアの雰囲気や活躍するメンバーの特徴は? エニグモの技術選定について エンジニアのキャリアについて どのようなキャリアアップの選択肢がありますか? マネージャーへはどのようにアサインされるのでしょうか? フロントエンドエンジニア、サーバーサイドエンジニアを明確に分けていないのも特徴ですよね 部長として大切にしていることはありますか?   まずは簡単に経歴や自己紹介をお願いします 前職は新卒で入社した SIer で勤怠管理や人材管理などのパッケージシステムの開発をしていました。会社の中でも比較的開発を担当できる部署でしたが、役職が上がると開発から離れ管理がメイン(電話片手にエクセルとにらめっこみたいな)となる為、開発に関わりつづけたい、Webに行きたいと思い転職を決意し、ご縁があり エニグモ に入社しました。 入社してかれこれ10年が経ち、現在エンジニアの部長を務めています。 エンジニア組織や開発体制について 現在のエンジニアの開発組織について教えて下さい 現在、エンジニアの組織は、業務委託として参画してくださっている方も含め約50名の組織となっており、4つのグループ(インフラグループ、データテク ノロ ジー グループ、アプリケーション開発グループ、グローバルグループ)に分かれています。 小澤さんが入社してから今までで組織はどのように変わりましたか? 入社した当時、エンジニア組織としては8人程でしたので、それから比べると組織はだいぶ大きくなりましたね。私が部長になってからは、社員15人くらいまでは部長以下の役職を設けず全員フラットな組織でしたが、組織の拡大とともに、各グループにマネージャーの役職を置き、現在の組織体制となりました。 データテク ノロ ジー グループはここ数年で新しくできた組織ですが、グループ立ち上げの経緯を教えて下さい データテク ノロ ジー グループは元々アプリケーション開発グループの一部で、性能改善やバックエンドの安定化などを担うチームでした。全社的にデータドリブンな環境が加速していく中で、データ収集や 機械学習 、検索性向上などのデータ領域に対して専門性の高いチームへ進化していき、今のデータテク ノロ ジー グループへとなりました。 これらの進化は会社側からの トップダウン の方針ではなく、現場のマネージャーからの ボトムアップ で組織が進化していった経緯があります。 開発体制の特徴は? 現在、大きく3つの機能(購入者向け機能、出品者向け機能、サービスインフラ)別チームに分かれて開発案件に対応しています。各機能別にエンジニア、デザイナー、ディレクター、データアナリスト、ビジネスサイド(CS・MD等)が組織横断でプロジェクトに アサイ ンされ開発を進める、機能別の開発体制になっています。 職種で役割を完全に分けてしまうのではなく、企画・設計段階からみんなで意見を出し合い、サービスや機能を作っていくのが特徴です。 機能別の開発体制に移行した背景やその後の変化はありましたか? 元々はプロジェクトへのメンバーの アサイ ンは、プロジェクトの難易度や特性とメンバーの得意分野、やりたいこと、稼働等を見て判断しておりました。 しかし、 BUYMA は1つのサービスとして成り立つために様々な機能が組み合わさっています。 例えば、購入者向けの機能(商品詳細ページやレコメンド、クーポン等)と出品者向け機能(出品管理・ショップ連携・お問い合わせ管理等)ではサービスの性質・要件や抱える課題は異なり、仕様は複雑になっているため、開発メンバー全員が全ての機能の特性をキャッチアップすることは難しくなっていました。 そこで、開発メンバーが機能( ドメイン )別に特化することで、専門性を持ち効果的にスピードを上げて案件に対応できるのではないかと考え、機能別開発体制となりました。 開発体制の変更によって、機能ごとにエンジニア一人一人が当事者意識を持って仕事に取り組むことができ、エンジニアもプロダクト開発の目標設定への責任感・コミット力があがったと思います。 BUYMA の開発を行う魅力は? まずは、 BUYMA というそれなりのユーザーがいる ECサイト ・大規模サービスに関われることでしょうか。やった事への影響も大きいですし、画面を変えれば良くなったとユーザーから褒めていただいたり、時には厳しいご指摘をいただくなど反応もありますし、 トラフィック もあるのでパフォーマンスチューニングのやりがいもあります。また、ローンチして17年を超える Webサービス なので、古いシステム・技術もあり直すことも多いので、そういうところが好きな方であればやりがいに感じる方もいるかと思います。 開発や組織の課題について 前段でもお話ししましたが、17年を超える Webサービス の為、古いシステムやレガシーな技術もいっぱいあるところですね(新しい技術もどんどん導入していますが)。リフレッシュしないと開発速度が落ちてしまうので、常に技術のアップデートは行う必要があります。 技術のアップデートをするにも、機能やデータベース等が複雑に絡み合っている為、1つの技術を変えると他の機能への影響範囲も大きいため、それぞれチューニングが必要です。そのあたり開発・運用サイクルを効率よく回すためにも、いくつかの改善案を検討しています。 上記のような課題もある為、新しいサービスや機能を開発したいというエンジニアだけでなく、レガシーな技術をモダン化したい、開発が上手くまわる仕組みや環境を作りたい・整えたいというエンジニアも組織全体としてスピードアップして開発する為には必要であり、活躍の機会があります。 エンジニア採用で大切にしていることや、活躍するメンバーとは エンジニア採用で大切にしていることはありますか? 新メンバーが入社した際にも、その人がやりたいことをやる方がきっといいと思っているので、「こういうことがやりたい」というモチベーションが高く、且つ自走力があり実行できる人がいいですね。実は、私も上からあれしろこれしろとあまり言われないので、エンジニアにもあれしろこれしろ言わないようにしています。やりたい人に任せることが一番いい結果を生むと感じています。 また、成長意欲の高い人や向学心の強い方にはオススメな環境です。 なぜなら、 BUYMA はさまざまな機能が絡み合うため、システムとして非常に複雑になっています。新メンバーが、特に若手メンバーがシステムの概要をキャッチアップ・理解するまでに時間がかかり若手に与える適切な課題・タスクを切り分けるのが難しい場面もあります。 「教科書で学んできたことが教科書通りにはいかず、開発する上であれもこれも詰め込まれるので、一気に10人くらいに殴られる感覚になる」と、ある若手メンバーが言っていたのが印象的です。 1つ1つ少しずつ成長したい人にとっては、最初は我慢が必要かもしれません。その分成長のスピードは早く、 BUYMA で開発ができればだいたい何でも開発できるようになると思うので、臆せずチャレンジしていただきたいですね。 エンジニアの雰囲気や活躍するメンバーの特徴は? 色々なキャ ラク ターの人がいますが共通項としては、みんなまじめで何事も一生懸命な頑張り屋が多い印象です。また、エンジニアに関わらず、 エニグモ のメンバーはいい人が多いと思います。 活躍するメンバーの特徴は、ア イデア がある人じゃないでしょうか。ア イデア というのは、新規の提案だけなく、定例や MTG 内でも自分の意見や考えを発言し、発言して終わりではなく行動が伴う方です。そういったメンバーが周囲からも信頼を得られ活躍しているように感じます。 エニグモ の技術選定について メンバーやマネージャーからの ボトムアップ で決まる場合が多いですね。 経営判断 や、エンジニアのマネージャー会議等で、これを導入していこうというよりは、 専門性のある各メンバーが今の課題の中からこの技術が良いのでないかと判断し導入します。 もちろん例外もあります。規模感にもよりますが、さすがに BUYMA 全体の言語を PHP から Ruby へ変更しようとなった時や、 BUYMA のインフラ環境をオンプレから AWS へ移行等の規模が大きい案件の場合は役員プレゼンし承認を得ています。そのほか、技術導入による影響範囲が大きい場合や、導入にお金が絡む際も私のところに相談が来ますね。 エンジニアのキャリアについて どのようなキャリアアップの選択肢がありますか? スペシャ リストとしてもマネージメントとしてもキャリアアップの選択の機会があります。 役職や肩書がなくても スペシャ リストとして高い報酬が得られる給与体系となっており、本人の志向や経験スキルに応じて柔軟なキャリアステップを歩んでいただけます。 マネージャーへはどのように アサイ ンされるのでしょうか? エニグモ のエンジニアの特徴としては、チームを作りたいという人よりも、 スペシャ リストになりたい人の方が多いように感じます。その為、マネージャーを任せたいメンバーには1on1等でやってみる?と聞いています。 一概には言えませんが、任せたいと思うエンジニアはすでにメンバーの役割を超えて、チームをまとめていたり、PM・リーダーとしての動きをしているので、そこからマネージャーをやってみようかとなるパターンも多いように感じます。意図的というよりも自然とそうなっています。 フロントエンドエンジニア、サーバーサイドエンジニアを明確に分けていないのも特徴ですよね はい。 エニグモ ではエンジニアの担当領域をフロントエンド、バックエンドではなく、サービス・プロジェクト単位で アサイ ンしている為、 フルスタ ックな知識・経験やスキルをつけることが出来ます。 なぜ分けていないかというと、フロントエンド・バックエンド両方できた方がやりがいや完成した際の達成感があると考えているためです。 せっかく画面があるサービスなので裏側から表に出てくるまでの一連の流れをやった方が楽しいと思うため、やりたいメンバーにはサーバーサイド、フロントエンドとわずにプロジェクトに アサイ ンし任せています。 部長として大切にしていることはありますか? みんなが楽( ラク )になればよいなと思っています。楽というのは業務が上手くまわる感じですね。 1on1でも、今の仕事は楽しいか、何をやりたいのかを聞くようにしています。何が好きで何がやりたいかを聞いてそれを実現できる環境を作ることが今の私の役割だと思ってます。 経営陣もエンジニア組織のみならず、各部門の意思を最大限尊重してくれる文化なので、部長としてもやりやすい環境だと思っています。   以上、エンジニア部長の小澤さんのインタビューでした! こちらで、 Enigmo Advent Calendar 2021 は以上となります。今年も色々な記事がありましたね。 2022年もよろしくお願いします! 株式会社 エニグモ 正社員の求人一覧 hrmos.co
こんにちは、インフラエンジニア の 加藤( @kuromitsu_ka )です。 この記事は Enigmo Advent Calendar 2021 の 24 日目の記事です。 今回は、オンプレミスの MySQL を、Auroraへ移行する際、困ったことと対応したことを記載します。 移行方式をざっくりいうと、オンプレミスの MySQL より取得した、論理バックアップ(mysqldump)とバイナリログを使用してAuroraへ移行しました。移行のため、リストア環境と、リストア後のデータのチェック環境を構築したので先にその説明を記載して、困ったところと対応を記載していきます。 リストア環境 オンプレミス MySQL サーバから取得した、バックアップファイルをAuroraへ適用する環境を作りました。EC2の MySQL と、Auroraとで レプリケーション を貼り、EC2の MySQL にデータを投入してリストアしました。バックアップファイルのダウンロードや、リストアジョブの スクリプト 実行は、リストアジョブサーバから実行します。 リストア後のデータのチェック環境 Auroraへのデータのリストア後に、データの差分確認をする環境として Memcached も用意しました。それぞれのテーブルのデータの合計 チェックサム 値を Memcached に入れて比較しました。こちらも、チェックのジョブ スクリプト 実行は、リストアジョブサーバから実行します。 困ったこといくつか。 本題のAuroraへの移行で困ったことは、5個あり、順を追って記載します。 やんごとなき理由で、Auroraへのリストアに物理バックアップが使えず困った。 Auroraの仕様上、バイナリログを直接適用できずに困った。 Auroraへのリストアで、バイナリログの適用に2日間もかかって困った。 リストア環境を作ったものの、バイナリのログ適用がコケて困った。 リストア後、バイナリログ適用したデータの時間がなぜか9時間ずれて困った。 困ったこと その1 やんごとなき理由で、Auroraへのリストアに物理バックアップが使えず困った。 Auroraへのリストアには、物理バックアップが、サポートされていました。こちらが使えると安心だったのですが、問題があり使用できませんでした。結果的に論理バックアップから、リストアすることとなりました。 公式ドキュメント 外部の MySQL データベースから Amazon Aurora MySQL DB クラスターへのデータ移行 物理リストアできなかった原因 オンプレミスの MySQL には、パラメータ( innodb_undo_tablespace )が設定されていました。Auroraでは、こちらが変更 不能 になっているため、物理バックアップでは、Auroraへのリストアでコケていました。パラメータ変更には、 MySQL サービスの再起動が必要なため、論理バックアップを使用することとなりました。 エラーログ Auroraのerror/ mysql -error-running.log、error/ mysql -error.logより確認したエラーログ [ERROR] InnoDB: Unable to open undo tablespace './/undo001'. [ERROR] InnoDB: Plugin initialization aborted with error Generic error [ERROR] Plugin 'InnoDB' init function returned error. [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed. [ERROR] Failed to initialize builtin plugins. [ERROR] Aborting 問題のパラメータ undo ログが分割するテーブルスペース数を設定するパラメータ( innodb_undo_tablespace ) 物理リストアできなかった値 - innodb_undo_tablespace = 2 物理リストアできる値 + innodb_undo_tablespace = 0 公式ドキュメント Aurora MySQL 設定パラメータ パラメータ名 変更可能 innodb _undo_tablespace いいえ 困ったこと その2 Auroraの仕様上、バイナリログを直接適用できずに困った。 Auroraへは、直接バイナリログ適用できない仕様で、リストア環境を用意することになりました。BINLOGコマンドは、スーパーユーザーの権限での実行が必要なのですが、Aurora では、スーパーユーザー権限を利用することはできないそうで、コケてしまいました。そのため、リストア環境としては、EC2に MySQL を作成して、 レプリケーション を貼りました。 ※他社事例では、バイナリログをデコードして生のクエリを直接Auroraに適用する方法もありましたが、移行作業をしていた当時は見ていませんでした...。 適用コマンド # mysqlbinlog --no-defaults --database=${DB_NAME} --start-datetime=${START_TIME} --stop-datetime=${STOP_TIME} bin-log.00xxx | mysql -h ${AURORA_ENDPOINT} -P 3306 -u admin -p エラーログ ERROR 1227 (42000) at line 7: Access denied; you need (at least one of) the SUPER privilege(s) for this operation 公式ドキュメント 13.7.6.1 BINLOG Statement Amazon Aurora MySQL DB クラスターと外部の MySQL データベースを同期する 外部のソースインスタンスを使用したバイナリログファイル位置のレプリケーションの設定 困ったこと その3 Auroraへのリストアで、バイナリログの適用に時間がかかって困った。 Auroraへのリストアでは、バイナリログ適用(1桁GB程度でも)に2日間ほどかかりました。こちらは、AuroraのDB インスタンス のマシンリソースの増強と、Auroraのパラメータを変更したところ高速化できました。結果、バイナリログ適用は、20分程度に納まるようにできました。 対応したこと AuroraのCPU、メモリ使用率を見つつ インスタンス タイプを変更 サーバ インスタンス タイプ CPU メモリ(GB) オンプレミス MySQL 12 252 EC2の MySQL (リソース増強前) db.t3.small 2 2 EC2の MySQL (リソース増強後) db.r6g.4xlarge 16 128 Auroraのパラメータより、バイナリログ出力を一時的にOFFに変更 Binlog_format = OFF 公式ドキュメント Amazon RDS インスタンスタイプ MySQL または別の Aurora DB クラスターとのレプリケーションの設定 困ったこと その4 リストア環境を作ったものの、バイナリのログ適用がコケて困った。 バイナリログ適用が タイムアウト したり、2,3割適用できたところでコネクションエラーになったりもしました。 デバッグ の際も、 タイムアウト とコネクションエラーしか出ず、原因がぱっと見ではわからずで困りました。結果として、リストア環境の MySQL パラメータを変更することで、解決しました。 エラーログ ERROR -- : Lost connection to MySQL server during query (Mysql2::Error::ConnectionError) MySQL パラメータ変更 リストア環境のEC2の MySQL で、パラメータチューニングを行いました。ひとまず、 タイムアウト 関係のパラメータを操作しましたが、一部バイナリログデータを適用できず止まってしまうものもありました。続いて、パケットサイズのパラメータも変更しましたが、エラーは続いていました。最終的に、プロセスが、十分なメモリを確保できていないのかなと考えて、メモリキャッシュ関係のパラメータを変更して解決しました。 タイムアウト 関係の MySQL パラメータ(ひとまず2日間様子見できるように設定) connect_timeout = 172800 net_write_timeout = 172800 net_read_timeout = 172800 wait_timeout = 172800 interactive_timeout = 172800 パケットの最大サイズの MySQL パラメータ(最大値に設定) max_allowed_packet = 1073741824 メモリキャッシュ関係のパラメータ(最大値に設定) ※変更したパラメータ「 innodb_buffer_pool_size 」は、読み込み、書き込みのパフォーマンス向上にも使われるパラメータです。 innodb_buffer_pool_size = 1G 困ったこと その5 リストア後、バイナリログ適用したデータのうち、タイムスタンプ関係のデータで、なぜか9時間ズレが発生して困った。 バイナリログで適用したデータのみ、何故か9時間ズレるという問題も経験しました。 MySQL の タイムゾーン 関係の、パラメータを変更して対応しましたが、解消しませんでした。結果として、バックアップ取得元の、バイナリログフォーマットのパラメータを変更して解決しました。 タイムゾーン のパラメータ オンプレミスの MySQL も、Auroraも、リストア環境のEC2の MySQL も JST に設定していました。 バイナリログ取得元の MySQL と、EC2に作成した MySQL のパラメータ +------------------+--------+ | Variable_name | Value | +------------------+--------+ | system_time_zone | JST | | time_zone | SYSTEM | +------------------+--------+ Auroraのパラメータ Auroraの場合、パラメータ「time_zone」が、データベースのデフォルト値になります。 +------------------+------------+ | Variable_name | Value | +------------------+------------+ | system_time_zone | UTC | | time_zone | Asia/Tokyo | +------------------+------------+ バイナリログフォーマットのパラメータ 「 binlog_format 」をMIXEDから、ROW変更 レプリケーション 元の binlog_format を ROW とすることで、バイナリログ適用の際、クエリでなく実行結果が レプリケーション されるようになるそうでした。バックアップ取得元の、オンプレミスの MySQL にてパラメータ変更した結果、無事、データが一致しました。 バイナリログを適用した、データのタイムスタンプのデータが9時間ズレたパラメータ - binlog_format = 'MIXED' バイナリログを適用した、データで時間のズレがなかったパラメータ + binlog_format = 'ROW' ドキュメント Amazon Aurora DB クラスターのタイムゾーンを変更するにはどうすればよいですか? 感想 オンプレミス MySQL のAurora移行は、たいへんでしたが楽しかったです。 明日の記事の担当は 人事総務 の 廣島 さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは。サーバーサイドを担当している橋本です。 この記事は Enigmo Advent Calendar 2021 の23日目の記事です。 普段は Ruby on Rails を書くことが多いですが、とあるプロジェクトでAirflowを使った既存バッチの性能改善を行いました。プロジェクトはAirflowについて全く知らない状態からスタートして学ぶことが多かったので、この記事でAirflowの紹介と開発の感想を書いていきたいと思います。 Airflowとは? Airflowとはワークフローの管理ツールで、あるタスクを実行したら次のタスクを実行するといった形で一連のタスクを管理するものになります。Airflowではjobの実行順や依存関係をDAGで定義していて、DAG自体は Python で作成されています。 簡単にDAGのコードのご紹介です。(環境構築は省略させていただきます。) 下記のプログラムは 文字列をprintするタスクを順番に実行するものです。 DAG で実行時刻やDAGの名前の設定をします。 PythonOperator を使ってタスクを定義し、一番下の行の execute_task1 >> execute_task2 で実行するタスクの順番を定義します。 import airflow from airflow.operators.python_operator import PythonOperator from airflow.models import DAG from datetime import datetime args = { 'owner' : 'airflow' } dag = DAG( dag_id = 'advent_calendar_tasks' , default_args = args, schedule_interval = '0 0 * * *' ) def task1 (): print ( 'task1' ) def task2 (): print ( 'task2' ) execute_task1 = PythonOperator( task_id = 'execute_task1' , retries = 2 , python_callable = task1, dag = dag) execute_task2 = PythonOperator( task_id = 'execute_task2' , retries = 2 , python_callable = task2, dag = dag) execute_task1 >> execute_task2 管理画面にアクセスするとDAGの情報を見ることができ、実際に実行することもできます。 使ってみてよかったこと モニタリングが GUI から管理できる DAGのスケジュールや実行状況を同一画面で見ることができるので、モニタリングがしやすかったです。また、DAGの実行やスケジュールの on/offの設定も管理画面からできるので、コマンドを実行する手間を減らすことができたのが便利でした。 タスク単位で確認できる 管理画面のDAGの詳細画面ではタスクを個別で見ることができます。プログラム作成時にタスクを上手く分割すればDAGが実行するタスクを簡単に確認できます。 タスク実行に便利なライブラリが提供されている 実際のタスクでは SQL を実行したりファイルをアップロードする必要があると思いますが、DAGで使用できるライブラリが提供されています。プロジェクトでは FTP をアップロードするために FTPHook というライブラリを使いましたが、 Python で標準で使える ftplib よりも簡単に FTP を扱うことができました。 使ってみて難しかったこと Python に不慣れだったこと プロジェクトでは Ruby on Rails => Python へ刷新したのですが、普段は Ruby を書いているので Python に慣れるのが大変でした。 セキュアな情報の設定が初見だとわかりにくかったこと Airflowではセキュアな情報をConnectionsで管理していますが、最初はDAGに接続情報を書いてしまいました。あらかじめドキュメントを読んでAirflowのコンセプトを理解する必要があると感じました。 まとめ ここまでAirflowの紹介と感想を書きましたが、開発中は学ぶことが多く新鮮な気持ちで開発ができました。 明日の記事の担当は インフラエンジニアの加藤さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、サーバーサイドエンジニアの Steven です。 この記事は Enigmo Advent Calendar 2021 の22日目の記事です。 今回は Vagrant 環境をリプレースすることとなった Docker 環境をどう早くしたかについて説明します。 スタート地点は Vagrant 環境 エニグモ では以前から VirtualBox と Vagrant によるローカル環境を使って、開発してました。 使い勝手は完璧ではなかったのですが、開発する分には問題がとくになく長年活用されました。 ただし、それは構築ができたらの話で、構築時間が長いのと、時間が立てば立つほど自ずと新しい構築エラーが発生して、随時対応しないといけない状態でした。 エンジニアの場合、超えられない問題ではなかったのですが、デザイナーなどテクニカルな知識がそれほどない方だと、サポートしてもハードルがかなり高かったです。 VM 内で使っていた OS も古いバージョンの CentOS で、いずれ更新しないといけなかったです。 Docker 環境ができました Docker で新しいローカル環境を作ることで以上の問題を解消できないかと動いてくださったエンジニアがいました。 そうすれば、構築時間の短縮と、安定性の改善、使い勝手の向上を実現できるからです。 構築して想定通り改善はできましたが、代わりに新しい問題が現れました。 それもよくあるパターンのようで、 Vagrant 環境よりパフォーマンスが悪く、使い物にならない環境になってしまいました。 ローカル環境とはいえデータセンターにあるサーバーとつなげたりするので、もともとの Vagrant 環境でもはじめからそれほど早くはなかったです。 なので、それ以上パフォーマンスが落ちると、対策が必須となってしまいます。 当時はチューニングを試みましたが、根本的な改善が見られず、Docker 環境の導入は一旦保留となりました。 レスポンスタイム比較 ページ Docker環境 Vagrant 環境 トップ 9.95s 1.07s 検索結果 9.22s 1.89s マイページ 9.30s 1.79s Docker Desktop for Mac について この場合 Docker 環境のパフォーマンスが悪かったのはコンテナーと macOS 間のファイル IO のパフォーマンスが悪かったからです。 アプリケーションコードをすべてメ モリー に保持するなど、ファイル IO がそれほど発生しないアプリケーションの場合は問題にならないこともあると思いますが、私達の場合はファイル IO がどうしても多く発生する環境なので、必然的にパフォーマンスが悪かったです。 Docker Desktop for Mac では、 Linux 環境と違って、コンテナーはそのまま macOS の カーネル に実行されておらず、 macOS 上で動く VM の中にある Linux カーネル によって実行されています。 なぜそうなっているかというと、 macOS の カーネル ではコンテナー化のサポートがなくて docker のようなコンテナーを実装することができないからです。 なので、bind ボリュームを通してコンテナー内から macOS 側にあるファイルにアクセスする時は、 VM の中から osxfs(レガシー)か gRPC FUSE という ファイルシステム レイヤーを通して、 macOS 側のファイルが読み込まれます。 ただし、抽象化が多いところから、ケースによってそのレイヤーがかなり遅くて、ネイティブのアプリケーションと比べ物にならないことが珍しくないです(当然といえば当然ですが)。 Docker Desktop for Mac でキャッシュのオプションもありますが、試した結果それほど影響が大きくなくて、違いに気づけるかどうかというレベルでした。 Docker チームでパフォーマンスの問題を認識していて、改善を以前から試していますが、 ファイルシステム の実装はかなり難しいもので、 トレードオフ が多いです。 パフォーマンスを高くするために工夫すると、整合性などの面で新しい問題が現れたりします。 パフォーマンスは改善傾向にありますが、満足の行かないケースがまだ多いと思います。 Mutagen とは Docker Desktop が提供するオプションだけでは解決できない問題なので、 サードパーティー による解決策を探しました。 最初は docker-sync を試しましたが、最終的に Mutagen に落ち着きました。 Mutagen はファイル同期とネットワークの フォワ ーディングのためのツールで、本来は クラウド にあるリソースをローカル環境で使うためのものかと思いますが、最近は docker compose のサポート が追加されて、docker 環境と合わせて使うことが可能になりました。 ファイル同期は rsync によるものなので、パフォーマンスがよくて、かなり堅牢なものです。 docker compose と合わせて使う場合は macOS とコンテナーの間に、 VM 内に同期されているファイルのコピーが用意されます。 コンテナ内から本来 bind であったボリュームへのファイルアクセスが発生した場合は macOS 側のファイルを読みに行かず、 VM のファイルにのみアクセスするようになります。 アプリケーションのファイル処理が VM 内で完結するため、 macOS と VM 間のファイルのやり取りが激減して、パフォーマンスの ボトルネック がなくなります。 Docker チームでも Docker Desktop に Mutagen を正式的に導入する動きが以前ありましたが、導入で追加の複雑さが生じることから、やめることとなったようです。その代わりに gRPC FUSE を優先するようになりました。 導入例 Mutagen の導入はかなり簡単です。 まずは brew で Mutagen をインストールします。 $ brew install mutagen-io/mutagen/mutagen-beta # 現在はβバージョンが必要です 続いて、 docker-compose.yml で macOS 側のファイルにアクセスするためのボリュームを用意します。 services : bm_on_rails : # ... volumes : - rails-source-sync:/bm_on_rails bm_php : # ... volumes : - php-source-sync:/home/web/bm_php volumes : rails-source-sync : php-source-sync : 最後に、同じファイルで、 x-mutagen の項目の配下に Mutagen の設定を指定します。 x-mutagen : sync : rails-source-sync : mode : 'two-way-resolved' alpha : './volumes/bm_on_rails' beta : 'volume://rails-source-sync' php-source-sync : mode : 'two-way-resolved' alpha : './volumes/bm_php' beta : 'volume://php-source-sync' alpha と beta は同期のエンドポイントとなります。 意味合いは mode によりますが、以上では alpha は macOS 側のパス、 beta は Docker のボリュームを指しています。 mode にはいくつかの選択肢がありますが、コンフリクトを自動解消するとして、 alpha の変更をどんな時も優先したい場合は two-way-resolved が適切です。 詳しくは こちら をご確認ください。 セットアップができたら、次は Docker 環境を mutagen compose up で立ち上げます(Mutagen の新しいバージョンでは mutagen-compose up )。 docker compose コマンドを使うと、Mutagen の処理がスキップされるので、間違えないよう注意してください。 ただのラッパーなので、 docker compose でできることは mutagen compose でもできるはずです。 ちょっと不便かもしれませんが、Mutagen の開発者側で docker compose をそのまま使えるように検討されているようです。 環境の初回起動に macOS 側のファイルが VM 内にコピーされるので、ファイルの量によって時間がかなりかかってしまう可能性があります(私達の場合は 10分ぐらい)が、二回目以降は Mutagen を使ってないのとあまり変わらなくなります。 導入後、アプリケーションのパフォーマンスは Vagrant 環境よりやや早くなりました。 データセンターへのアクセスがどうしても発生するので、そのパフォーマンスで目標を達成としました。 注意 Mutagen は Docker Desktop のバージョンに依存していますので、Mutagen のバージョンと Docker Desktop のバージョンに気をつけてください。 Docker Desktop のアップデートが来る度にすぐアップデートすると、Mutagen が動かなくなってしまう恐れがあります。 Mutagen の docker compose サポートはまだβですが、バグがほぼなくとても安定しています。 調整 デフォルトで macOS 側のファイルすべてが VM 内に同期されるので、 .git ディレクト リなど VCS 用のファイルを同期したくない場合は追加の設定が必要となります。 任意のファイルの同期をスキップすることも可能です。詳しくは こちら をご参照ください。 x-mutagen : sync : defaults : ignore : vcs : true # ... 特に設定がない状態では Mutagen に同期されているファイルのオーナーとグループ、 パーミッション はコンテナー内でデフォルトなものとなってしまいます(オーナーとグループはおそらく root となります)。実行権限のみ同期されます。 なので、コンテナー内のファイルのオーナーとグループ、 パーミッション を調整したい場合は追加の設定が必要となります。 詳しくは こちら をご確認ください。 x-mutagen : sync : # ... php-source-sync : # ... configurationBeta : permissions : # php コンテナー内ではファイルのオーナーとグループを apache にする defaultOwner : 'id:2000' defaultGroup : 'id:2000' 同期オプションは他にも いろいろあります ので、必要に応じてご確認ください。 同期セッション重複問題 Mutagen を導入してから、社内で特にファイル同期に関する問題が報告されなかったのですが、少しずつ、 MacBook の CPU 使用率が高い、見覚えのない差分が git status に出てる、などと相談が来るようになりました。 差分の問題はファイル同期と関係がありそうだと思ったので、その方向で調査を進めたら、相談者の MacBook で mutagen sync list が本来2つしかないはずのセッションを大量出力しました。 問題出力 -------------------------------------------------------------------------------- Name: rails-source-sync Identifier: sync_93JIPMqNq5WkYV20nV9Wq4XdvyBr3CXz3oonMfIkyYQ Labels: io.mutagen.compose.daemon.identifier: JH67_K5QB_5FG6_F4UH_OG45_7EKG_LBBY_7IPY_Z4ME_IKQY_HVSG_PPTU io.mutagen.compose.project.name: docker_buyma ... -------------------------------------------------------------------------------- Name: php-source-sync Identifier: sync_bzryoXJaLbxevdit2ODkZuGz2RChyN2C2W5wS8CdbdU Labels: io.mutagen.compose.daemon.identifier: JH67_K5QB_5FG6_F4UH_OG45_7EKG_LBBY_7IPY_Z4ME_IKQY_HVSG_PPTU io.mutagen.compose.project.name: docker_buyma ... -------------------------------------------------------------------------------- Name: php-source-sync Identifier: sync_FaC9uwjuhGziEeggNVbPI2EFgGUE1sxtKArzva4rSck Labels: io.mutagen.compose.daemon.identifier: T3PW_AONQ_MWDI_T5BO_Z6EH_6PQB_6CJZ_336T_M2KO_AXQH_ZSAQ_DQ7E io.mutagen.compose.project.name: docker_buyma ... -------------------------------------------------------------------------------- Name: rails-source-sync Identifier: sync_tPPnFvmEjlwKhLtkrudgukM3Qc7AHdTOc0QANYjwAmN Labels: io.mutagen.compose.daemon.identifier: T3PW_AONQ_MWDI_T5BO_Z6EH_6PQB_6CJZ_336T_M2KO_AXQH_ZSAQ_DQ7E io.mutagen.compose.project.name: docker_buyma ... -------------------------------------------------------------------------------- ... 出力を見てわかりますが、同期セッションが重複しています。 設定は一緒ですが、 daemon.identifier というものだけがそれぞれ違います。 daemon.identifier は Docker デーモンの id です。 デーモンはもちろん一つしかなくて、再起動しない限り id も変わらないはずです。 問題は Docker 開発環境を終了せず、 MacBook を再起動すると、発生していました。 原因としては再起動前に Mutagen のセッションを終了しなければ、再起動後に Docker 環境を立ち上げた時、古い同期セッションが残っていながらも、Docker デーモンの id が変わった影響で、同期セッションがまだ作成されてないと Mutagen が判断して、新しい同期セッションを作ってしまうということでした。 該当するイッシューはあります(問題を解消できないか検討中のようです)。 https://github.com/mutagen-io/mutagen/issues/243 対策としては Docker 環境起動後に mutagen sync list の出力を確認して、重複したセッション(現在の Docker デーモン id を使ってないセッション)があった場合、 mutagen sync terminate でそのセッションを終了するように スクリプト を作成しました。 MacBook 停止の際に Mutagen の同期セッションを必ず終了するようにするのも考えられる対策です。 M1 対応 新しい MacBook で ARM アーキテクチャ ーの M1 チップを使うことで macOS の業界で動かなくなってしまったものが多くあります。 なので、M1 対応をした時はもしかすると Mutagen が動かなくなってしまうと懸念しましたが、問題なく動きました。 インストールで調整は必要なく、同期も支障なく行われていますので、M1 で使う分には問題ないと思います。 docker-sync について Mutagen を使うようになる前に 0.5.1 の docker-sync をまず試しました。 docker-sync は ruby の gem と unison を生かした、Docker Desktop 専用のファイル同期ツールです。 仕組みも設定方法も Mutagen に似ていますが、Mutagen と違って macOS 側で動くプログラムが多く、rbenv/ ruby などのインストールが必要です。 Vagrant での環境構築ではそれらのインストール時に様々な問題が発生していたため、今回は rbenv/ ruby などのインストールは避けたかったです。 docker-sync で初回同期は問題なくて、パフォーマンスも Mutagen と同じぐらい改善されましたが、ファイル同期が不安定で、 macOS 側でファイルが変わっても、コンテナー内に反映されないことが多々ありました。 ファイル同期を強制するにも docker-sync のデーモンを再起動するしかなく、そうする度に CPU 使用率が跳ね上がって、 MacBook がドライヤーなみにうるさくなったりしていました。 docker-sync のイッシューを確認したら、開発者が問題を認識していても解決策が思いつかない状態のようでした。 なので、以上のことから docker-sync はあまりおすすめできません。 終わりに Docker Desktop for Mac のファイル同期のパフォーマンスの悪さで悩まされた期間が割と長かったのですが、Mutagen を導入することで完全に解消して、デメリットもほぼないので、同じ悩みを抱えられているなら、ぜひ導入をご検討ください。 Docker チームによる Docker Desktop のパフォーマンス改善に期待したいところですが、Mutagen レベルのパフォーマンスが実現されるまでどれくらい時間がかかるのかがわからない状態なので、そうなるまで サードパーティー に頼るしかないかと思います。 明日の記事の担当はエンジニアの橋本さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、Corporate IT/Business ITを担当している足立です。 この記事は Enigmo Advent Calendar 2021 の 21日目の記事です。 代打として2回目の登場です。 2021年の前期はオフィスリニューアルPJの業務に追われていました。 コロナ禍に突入し出社とリモートワーク両方に最適化された環境を構築する事をミッションとして動きましたので、 今回はコーポレートIT目線で実施した事を書きたいと思います。 旧オフィスの課題 電源問題 リニューアルするなら旧オフィスで課題だった部分を改善したいと思いました。 総務的な話になりますが、特に電源については下記問題がありました。 電源 旧オフィス時にはOAタップが少なくタコ足配線かつ数珠繋ぎ状態で、あちらこちらにあり危険な状態 電子レンジを2台同時に利用した際にブレーカーが落ち iMac が強制シャットダウン状態になった事がありアンペア増強・単独系統への見直しが必要 → リニューアル後 座席毎に6個口OAタップが設置されました。 6個口のOAタップ 全てではありませんが、座席にモニターを常設しUSB-Cによる電源供給も可能にしました。 最大65 Wの電力を供給出来るので、PCの電源供給と映像信号が1つのケーブルで対応可能になりました。 DELL P2419HC オフィス レイアウト作成時に社内に設置する電子レンジは執務エリアと電源系統と別にして頂きました。 絶対にサーバールームの電源系統は他に影響しないように手配しました。 電話周り 電話システムはオンプレ型のPBX(レガシーPBX)で構成されていたため、頭を悩ませていました。 これも総務的な話ですが、気づいたら電話周りも担当していたので、どうにかしようと思いました。 課題 オフィスに設置する物理的な機器であるため、移設時には工事などが必要(PBX・配線) 運用保守は専門の業者に依頼をしている 今後、移転の場合は通信キャリア、専門業者との調整が必要になり時間がかかる 当然、電話は社内でしか使用出来ない PBXがサーバールームに設置してあり場所を取る(兎に角、邪魔でしょうがない) 旧 電話構成 → リニューアル後 クラウド PBXであるDialpadを導入しました。 Dialpad 導入した事によりPCや スマホ で会社の電話を利用出来るようになりました。 着信については会社が 保有 している電話番号(03番)にて着信するように、キャリア自動転送を利用してDialpadで発行された050の番号へ転送するようにしました。 ただし、発信はDialpadで発行した050の番号で発信する事になります。 シン・電話構成 最近、Dialpadでも0ABJ番号(03番)を別途、機器を設置せずにライセンス購入のみで利用出来るようになりましたので03番で発着信したい場合は、そちらを利用すると良いと思います。 導入後はWEB上で電話関連の設定が出来るようになりました。 転送機能 留守番電話 IVR設定(音声ガイダンス) 他にも色々と機能がありますが、特にIVRが利用出来るようになったので内容に応じて BUYMA カスタマーサポートやfondeskへ誘導出来るようになりました。 キャリア自動転送をする際に エニグモ の固定電話回線は ISDN だった為、キャリアに依頼してオペレーターの方に対応してもらうか電話機の操作で転送設定をする必要がありました。 ただ、従来の電話機は処分する必要もあり電話機の操作による設定は利用出来なくなり、かと言ってオペレーターの方に依頼する場合、変更まで日数がかかるので思い切って電話回線を 光収容 化しキャリアのWEBページ上で転送設定が出来るようにしました。 IVR構成一部 (この構成はモダンな情シス Kajinariさんの事例を参考とさせて頂きました。) IP固定電話 電話機は完全に無くすつもりで考えていましたが、バックオフィス部門から電話機が必要と言われたので 5台 POLYCOM VVX 350を設置しました。 VVX350 こいつが厄介だったのは、日本国内では購入は可能だが、サポートが受けられないのが難点でした。 お取引があるベンダーさん何社かに問い合わせしましたが、どこも販売までしか対応出来ないと言われ 覚悟を決めた上での購入でした。 不明な部分については基本、英語のマニュアルを翻訳して調べたり勘に頼ったりして設定しました。 それでも分からない部分についてはDialpadサポートの方が教えてくれました。 FAX コロナ禍直後は 複合機 から受信したFAXをメール送信→Zapier→Slackで通知、 Google ドライブに格納と言う構成でしたが、課題もありました。 複合機 の仕様上、用紙が切れるとFAXのメール送信が出来ない 送信が社内のみでしか利用出来ない PBXを撤去したいから電話線引きたくない 課題を無くす為、 KDDI のペーパーレスFAXを導入し クラウド 化を行いました。 当初は「FAX.PLUS」の導入も検討していましたが、FAXを利用する業務の大半が 経理 チームで主に銀行とのやり取りだったので、何かトラブルがあった際に日本企業の方が意思疎通が早いのかなと思い KDDI にしました。 WEB上でFAXの送受信が出来れば支障は無いので、特にこだわりはありませんでした。 シン・FAX システム構成 導入後、FAX受信はメール添付機能を利用し、旧構成と同じくZapierを利用してSlack上に受信通知、 Google ドライブへ保存するようにし FAX送信時はペーパーレスFAXのサイトにログインして使用するので自宅からでもFAX送信出来るようになりました。 受付システム・座席予約(ホテリング) 電話の話にも通ずるのですが、受付をどうするかも課題でした。 従来の受付ではエントランスに電話機を設置し総務担当が取次するスタイルで お客様がお見えになる際は「ご来社カード」と呼ばれる入退室管理の用紙に必要事項を記入して頂くフローでした。 ただ、このままだと下記課題が残ります。 受付の内線電話を撤去しなければオンプレPBXを撤去出来ない 外来者の入退室管理が紙による記入かつ集計作業が業務負担になっている 旧エントランス そこで「ACALL RECEPTION」を導入しました。 導入後、来客通知はSlackに変わり、お客様がお見えになった際は別途、設置したラベルプリンターから入館証が発行されるようになり 内線電話による受電対応が無くなり、入館履歴がシステム化され集計作業も不要になりました。 シン・エントランス ただ、一部部署にて宅配業者がお見えになった際にSlackの通知だと作業中に気づかないと言う相談があった為 宅配業者専用ボタンを作成しACALLとDialpadを連携、そこだけはIP固定電話にて着信する運用となりました。 座席予約システム(ホテリング) 新しいオフィスは面積が今までの半分になる為、 フリーアドレス を採用する事になりました。 リモートワークが主流になり、座席数がそこまで必要無くなりますが、その際に上司から座席予約システム導入を検討するよう指示がありました。 フリーアドレス フリーアドレス 2 個人的には必要なのか?と思いましたが、席のダブルブッキングや出社している人をひと目で可視化する為にも必要でした。 また、WEB MTG 用に個室ブースを8箇所設置する為、座席予約システム導入がマストになりました。 WEB MTG 用個室ブース 導入検討をしていた当時、ベストなソリューション選定に大変悩みました。 現在はコロナ禍を想定した座席予約システムが増えて来ましたが、当時としては・・・ 社内のコミュニケーション 円滑化を目的としたシステム(抽選式で座席を決めるシステム) IoTに特化した座席予約システム(各座席にセンサーを設置する為、費用も高額、電池交換も必要) 弊社としてはWEB上か スマホ で予約してはい、終わり!ぐらいなシンプルな機能を求めました。 ただ、上記の様に希望する要件と違ったシステムばかりでした。 悩んでいる矢先に受付システムで導入しようとしていたACALLに座席予約(ホテリング)機能がローンチされるとの情報が耳に入りました。 内容を見てみると希望する要件を満たすのではと感じ、受付システムと一緒に導入してみました。 座席マップ 一部 導入当初は予約した席にチェックインする作業に慣れない人も居ましたが、現在ではすっかり慣れました。 チェックインとは事前に予約した席に来たらACALLの スマホ アプリで座席に貼ってある QRコード を読み込む作業です。 予約した時刻から15分以内にチェックインしないと予約がキャンセルされます。 この機能が無いと予約したにも関わらず席を利用しない人が現れた際に他の人が利用する事が出来ないので重要な機能です。 特にWEB MTG 用の個室ブースは1時間単位で利用する事が多く、システムの利用頻度は高めです。通常予約システムを導入した会社によっては利用しなくなるケースがあると聞いたことがありますが、弊社は業務に欠かせないシステムとなりました。 入退出システム 同時進行でシステム導入や移設手配をしている矢先に今度は入退出システム導入の話が出てきました。 オフィスの一部分だけは入居しているビルが管轄している入退出システムを利用しているのですが、C工事に該当するエリアについては自社で入退出システムを入れる必要がありました。従来ではセキュリティ会社が販売しているオンプレのシステムを利用していましたが、このタイミングでリプレイスする事となりました。 (管理コンソールが IE でしかログイン出来ない・・・) 代表からは「顔認証」で入退出出来るようにして欲しいとオーダーがありました。 当初は クラウド タイプの入退出システムをベースに顔認証を付ける構成を考えていましたが、お見積を取ると費用がかなり高額になる事が分かりました。 顔認証を諦めてもらい、 スマホ アプリや FeliCa を利用したシステムを役員へ提案しました。 プレゼン時に代表からは「顔認証」を捨てきれない印象があり、再検討するよう指示があり 「色々、見た上で提案しているんだから、他にあるんだろうか・・・」と思っていたので思わず「じゃあ、おすすめのシステムあれば見てみます」と 逆質問に近いように返答したところ、代表の口から「Safieとか」と言われ早速、見てみると「Safie Entrance」なるものが。 クラウド 録画カメラのシステムとしては知っていたのですが、顔認証での入退出システムがあるのは知りませんでした。 早速、連絡を取り仕様や費用面を確認すると従来の顔認証システムに比べて圧倒的に安価に導入でき月々の費用もかなり安くなる事が分かり導入が決定。 若干、「こんなに良いものあるなら早く言ってよ」と思いましたが、代表のひと言が無ければ導入出来なかったと思います。 SafieEntrance エニグモ では5箇所に設置しました。事前にSafieEntranceで使用する iPad を準備しケーブルを最小限にする為に電源はPoEにて供給する事にしました。 通信についてはテストした結果、 Wi-Fi でも動作は問題ありませんでしたが有線LANで行っています。以前は鍵を使って物理的に入退出していた場所は全て顔認証になりエリア毎に入退出出来る従業員を制限しています。導入してみると顔さえあれば入出出来るのでが大変便利です。以前だと鍵にかけ忘れなどあり、それが無くなったので物理的なセキュリティレベル向上にも繋がりました。 個室ブース 以前に比べWEB MTG の需要が増えました。 エニグモ でも基本、 MTG はZoomで行う事が増え、その度に会議室が不足する事になりました。 その為、上司の案で個室ブースを設置しました。 個室ブース 設計当初、防音になると聞いていたので各ブースにはLANケーブルを敷設しました。昔、ITサポートの仕事をしていた時にあるお客様のご家庭に防音部屋があり、その部屋だけ Wi-Fi が受信出来なかった経験がありました。使用する材質によっては著しく Wi-Fi の強度が低下する恐れがあったのでお守り代わりに引くことをリク エス トしました。 設置後に通信が不安定になるとWEB MTG に取っては致命傷になる事を危惧していました。実際に設置後にZoomを利用した際にインターネットの速度が不安定ですと表示された事があり、やっぱり引いといて正解だと心から感じました。 現在はオフィスに必要不可欠な場所となり、出社が多い日は予約出来なくなる事もあります。 もし個室ブースを設置を検討している場合はブース内の空調と照明にはこだわった方が良いと思います。 密封されている空間なので空調を重要ですし、窓が無い場合は照明を明るく出来るようする事をオススメします。 最後に 社内ネットワークについて書こうと思いましたが基本、現状維持のままでの移設だったので特段書く事も無く割愛しました。 ただ、Wi-Fi6に対応させようかと思ったのですが、サポート切れか今後会社が移転する機会があればその時で良いかと思ったので何もしませんでした。 あとは社内のAV機器周りについても担当したのですが、それはまた別の機会にでも書きたいと思います。 (これで1本書ける内容なので。) 新しいオフィスになり半年以上経過し、日々働いていると本当に良いオフィスになったと実感します。 このタイミングで様々なものを導入させて頂いて、短期間で担当した事は自分に取って財産になりました。 そして、導入に関してGoサインを出してくれた上司、役員の皆様に感謝の気持ちでいっぱいです。 明日の記事の担当は サーバーサイドエンジニアのstevenさんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
Next.js + Material UI v5 でフロントエンドアプリケーションを作成する なぜこの記事を書いたのか こんにちは。 エニグモ でサーバサイドエンジニアをしております、寺田( @mterada1228 )です。 この記事は Enigmo Advent Calendar 2021 の 20 日目の記事です。 業務では主に Ruby on Rails を使っているのですが、最近新しいチャレンジとして、フロントエンドの勉強をしています。 そこで、Next.js + Material UI(以降 MUI)を使った Web アプリケーションの開発にチャレンジすることにしました。 ただ、この2つは全く異なる開発形態の フレームワーク ですので、一緒に使うためにはちょっとしたセットアップが必要になります。 参考にするため色々と検索してみましたが、なかなか求めている検索結果が得られなかったことと、なぜこのようなセットアップが必要になるかまで詳しくまとめられているものがなかったので、今回自分で調べて一つの記事を書いてみることにしました。 なお、本記事では Next.js で MUI を使うためのセットアップに焦点を絞っているので、Next.js のプロジェクトを立ち上げる部分は説明しません。 その部分を詳しく知りたい方は、 公式チュートリアル を参考にしてもらえると良いかと思います。 Material UI を使うために必要なパッケージをインストールする MUI v5 のパッケージをインストールしていきます。 ひとまず、コアな機能をインストールすれば十分かと思いますので、 @mui/material というパッケージをインストールしていきます。他のパッケージについては、必要に応じてインストールして下さい。 また、MUI ではスタイリングのために内部で CSS in JS のためのライブラリを使用しています。 こちらは、 emotion もしくは styled-component のいずれかを入れる必要があります。 emotion を使う場合 // with npm $ npm install @mui/material @emotion/react @emotion/styled // with yarn $ yarn add @mui/material @emotion/react @emotion/styled styled-component を使う場合 // with npm npm install @mui/material @mui/styled-engine-sc styled-components // with yarn yarn add @mui/material @mui/styled-engine-sc styled-components next/link, @mui/material/Link の統合 Next.js からは、 next/link 、MUI からは @mui/material/Link という コンポーネント が提供されており、名前からお察しの通りどちらもカスタマイズされたリンク(a タグ)を生成します。 この2つには競合する部分もありますが、ある場面では next/link を、また他の場面では @mui/material/Link を用いた方が便利というケースがあります。(どういった場面でどちらを使うべきか、という理由はサンプルコードの後で説明します。) なので、状況に応じてどちらかの コンポーネント を返してくれるように、独自の Link コンポーネント を作成します。 src/Link.js ここで重要になってくるのは65行目から86行目の部分で、href に設定された URL が、 internal か external のどちらであるかを判定して、 return する コンポーネント を分けている、ということです。 URL が internal な場合は、Next.js が提供する、 next/link を返します。 これは Link がアプリケーション内のページ遷移であるときは、以下のような、Next.js が提供する各種機能を使用することができるからです。 Client-Side Navigation Code Splitting and Prefetching これらの説明は本記事の趣旨から逸れますので省きますが、詳しくは こちら をみて頂くと理解することができると思います。 URL が external な場合は、 next/link である理由はあまりないので、豊富なスタイリング用の Props が使用できる @mui/material/Link を使うようにします。 実際にリンクを作成する時は、ここで作成した、 src/Link をインポートする形になります。 pages/Index.js 独自 Theme を作成する こちらは作成せずとも、MUI の default Theme を利用できますが、多くの場合アプリケーション独自の Theme を設定していくことになると思いますので、本記事でも取り上げていきたいと思います。 今回は src 配下に Theme ファイルを配置していきます。例は簡単のために、 Typography の font-size だけを設定したものになります。 src/Theme.js 独自で定義した Theme は、 ThemeProvider を使って、アプリケーションに適用させることが可能です。 具体的な実装方法としては、 _app.js にて、 Component より外側の コンポーネント に ThemeProvider を設定してあげれば OK です。 pages/_app.js app.js, document.js の修正 最後に、Next.js で MUI を使用するために、 _app.js と _document.js の設定方法についてお話ししていきます。 はじめに、これから行う設定が何のためのものかというのを説明します。 MUI は元々、サーバサイド レンダリング されるものという制約のもと開発されています。 なので Next.js をはじめとした、クライアントサイドで レンダリング する可能性がある フレームワーク を使用した場合に不具合が発生してしまいます。 具体的にどのようなことが起きるかというと、いわゆる FOUC( Flash Of Unstyled Content)というもので、画面上に CSS の当たっていないページが一瞬表示されてしまいます。 これはクライアントサイド レンダリング 時に、HTML だけ表示されて、後から CSS が注入されるような流れになるため発生します。 なので、大まかに以下のような手順を踏むことでこの問題を回避していきます。 サーバサイド レンダリング 時に一度、 コンポーネント ツリーの レンダリング を行う そこから CSS を抜き出す( <style> を抜き出す) 抜き出した CSS をクライアントに渡してあげる この手順では以下のパッケージが必要となりますので、事前にインストールしていきます。 // with npm $ npm install @emotion/server @emotion/cache // with yarn $ yarn add @emotion/server @emotion/cache まず、 _document.js から説明していきます。 _document.js は Next.js においてあらゆる コンポーネント の初期化時に実行される コンポーネント ですが、その特徴として、サーバサード レンダリング 時に、サーバサイドで実行されるという特徴があります。 要するに前述した、1, 2 の手順をここで実行していくわけです。 サンプルコードは以下になります。 pages/_document.js 30行目からの処理に注目していきます。 getInitalProps はサーバサイド レンダリング 時に、ページにあらかじめサーバから取得した情報を埋め混むことができるメソッドです。 getInitialProps 内の60行目から66行目までの部分で、一度ページ コンポーネント の レンダリング を行なっていきます(前述した 1. の手順に相当) 以降の処理で、 レンダリング された コンポーネント から CSS ( <style> )を抜き出し、 emotion/cache を使ってキャッシュに保存します。(前述した 2. の手順に相当) 次にクライアントサイドの処理について見ていきます。 _app.js は _document.js 同様に、あらゆる コンポーネント の初期化時に実行される コンポーネント ですが、クライアントサイド レンダリング 時には、 _document.js は実行されず、 _app.js しか実行されないという特徴があります。 ここでは、 _app.js でサーバサイド レンダリング 時にキャッシュに保存した CSS を受け取るといった方法で、クライアントサイド レンダリング 時に発生する FOUC を回避します。以下がサンプルコードです。 11行目でサーバサイド レンダリング 時に保存した CSS を取得します。 このキャッシュに保存された CSS は17行目のように、 <Component> コンポーネント の外側を <CacheProvider> で囲ってあげることで、各ページ コンポーネント に渡されます。 ここでお話しした内容は、 MUI の公式ドキュメント でも詳しく説明されていますので、お時間ある方は合わせて見て頂けると理解が深まるかと思います。 おわりに 今回紹介したサンプルコードですが、 https://github.com/mui-org/material-ui/tree/master/examples/nextjs で MUI が公式に提供しているものです。今後変更がある可能性もありますので、サクッとコピペして使いたい方は、紹介したリンクから取得いていただくのが確実と思います。 明日の記事の担当は データテク ノロ ジー グループの堀部さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは。Corporate IT 所属の田中です。この記事は、 Enigmo Advent Calendar 2021 の19日目の記事です。本記事では、 Google Apps Script(以下、「GAS」といいます。)を使用して統合認証基盤サービスであるOktaの管理業務を効率化したことについて紹介したいと思います。 Oktaのグループにユーザーを追加する際、ユーザー数が多いと管理コンソール上で行うのに手間がかかり大変かと思います。 また、作業自体は簡単なものであるため業務アシスタントの方にお願いしたいと思いつつ、ロールの仕様上グループへのメンバー追加のみ権限を付与することも難しく、必要以上の権限付与を防ぐため結局自分で作業してしまうということもあるのではないでしょうか。 上記を改善するため、GASを用いて Google スプレッドシート 上で簡単にメンバーのグループ追加を行えるようにしました。 更に、DB化された スプレッドシート を利用して、Okta ユーザーのステータス情報を毎月コラボレーションツールのSlackに通知することで、月次のアカウント棚卸を効率化しました。 構成 仕組みはざっくり下図のとおりです。 *1 *2 ユーザー及びグループ一覧の取得 Oktaの API を使用してグループへユーザーを追加するためには、グループとユーザーそれぞれ一意に付与されているIDが必要です。 そのため、プロセスの前段としてGASでOkta API からユーザー情報一覧とグループ情報一覧を スプレッドシート に同期しています。同期は、GASのトリガーを定期的に実行することで実現しています。 下図が、 スプレッドシート へ反映されたユーザー一覧及びグループ一覧です。 「users-list」シートでは、ステータス、氏名、ローマ表記、email、ユーザーID、最後にログインした日時を取得しています。 また、group-listでは、グループ名、グループID、グループの説明を取得しています。 グループ追加 プロセスの後段として、上記取得した情報を基に、 スプレッドシート 上でOktaユーザーのグループ追加を行います。 下図左表「users-list」に追加する人の氏名を、右表「group-list」に追加先グループ名をそれぞれ入力すると、 スプレッドシート のvlookup関数を利用してid等が自動入力されますので、後は下図右側の画像をクリックすると、GASでOkta API を使用してグループへのユーザー追加プログラムが実行されます。 実行後、下図のように正常に追加されたことを確認しました。今後は、Slackの関連ページから本 スプレッドシート を開いて簡単にグループへ多数ユーザーの追加ができそうです。 Oktaユーザーのステータスを月次棚卸 Oktaユーザーに関するステータスやログイン状況を利用して、 スプレッドシート 上で集計し月末にSlackに通知することにより、棚卸作業がやりやすくなりました。 最後に いかがでしたでしょうか。私自身は非エンジニアで公開できる程綺麗なコードは書けませんためここでは非掲載とさせていただきますが、そんなレベルでも上記のような実装ができるぐらいGASは社内ITの連携に活用しやすく親和性のあるサービスかと思います。本記事の実装内容についてご興味ある方はご連絡くださいませ~。 また、enigmo Corporate ITでは各種 クラウド サービス間の連携・自動化を実装できるようなコーポレートエンジニア及び部門を横断的にセキュリティ整備を行うセキュリティエンジニアを募集しております!ご興味ある方は下記求人からご応募お待ちしております! 明日の記事の担当は、サービスエンジニアリング本部の寺田さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co *1 : Oktaのロゴは、次の 利用規約 に基づき使用しております。 https://www.okta.com/terms-of-use-for-okta-content/ *2 : Google 製品のロゴは、次の 利用規約 に基づき使用しております。 https://about.google/brand-resource-center/brand-elements/#product-icons
こんにちは、 BUYMA デザイナーの本田です。 BUYMA では今年の2月〜7月にかけてカートの大幅なリニューアルを行いました。 PHP から Rails への洗い替えや、パフォーマンスの改善がメインの施策だったのですが、UXの改善として、 上部に追従する購入ボタン や よくある質問エリア を新しく追加しています。 ユーザーからよくある実際に届いている声の中から、 購入ボタンが遠くて見づらい キャンセルや返品がわかりにくくて不安 配送が遅い エラーがわかりにくい 上記4つの課題を解決するために、どのような想いのもとでデザインに落とし込んだのかについて紹介します。 1. 購入ボタンが遠くて見づらい BUYMA はカート画面でユーザーに確認していただきたいことが多く、1商品あたりの高さができてしまい、購入ボタンまでスクロール回数が多いことが課題となっていました。 複数商品をカートに入れていても、購入ボタンを見失わないでもらうために 要素の見せ方整理をして、1商品あたりの高さを低くする 購入ボタンを上部へ固定し、スクロール時も追従するようにする の2つを行いました。 2. キャンセルや返品がわかりにくくて不安 海外で購入した商品を日本へ送るCtoCショッピングサイトという特性上、 偽物ブランドはないか 返品はできるか 送料や関税の負担はだれがするのか などの質問をユーザーからいただくことが多く、専用の説明LPや ガイドライン などで説明しているのですが、それでも購入前になかなか気づけない・ユーザーの不安を拭えないことが、課題となっていました。 解決策として、 LPへ遷移しなくてもカートページ内で補償サービスの内容を理解できるようにしたり 、 ユーザーからよくある質問エリアをまとめたエリア設置 しました。 3. 配送が遅い 海外配送となるため、送料の1番安いプランだと、繁忙シーズンには2週間以上かかってしまうこともあります。一方で、オプションメニューで、DHLやEMSなど海外の速達サービスを利用すると、+1,000円ほどで配送日数は大幅に短縮され、数日でお手元に届きます。 今までのUIですと、オプションメニューを選ぶことで、数日で到着することに気づかないまま、ご購入されるケースも多くありました。そのため、最短のプランがある時は、オプションメニューを選びやすくするUIパーツを追加しました。 4. エラーがわかりにくい 今回のプロジェクトを機に すべてのエラー文言を洗い出し 、どの状況下でエラーが発生してしまうのかを洗い出して整理し、提示するようにしました。 また、ユーザーは次に何をすればいいのか、 ネクス トアクションをきちんと提示することで、離脱率の低下を狙いました。 検証 1. 購入ボタン 購入ボタンのクリックの割合は 埋め込み:追従=8:2 くらいで予想以上に利用されていました。 以前に他のプロジェクトで、商品詳細ページでも追従ボタンを設置してABテストをしていたので、あまり期待はしていなかったのですが、購入をある程度決めているカート画面には適切だったということがわかりました。 2. キャンセルや返品についてわからなくて不安 よくある質問エリアのクリックが 全体の約2%で 、割合としてはそこまで高くないですが、あんしんしてお買い物をするための後押しに少しでも貢献できたと思うと、デザイナーとして手応えがあったなと思います。 3. 配送が遅い 「最短の配送方法あり」の ツールチップ が表示されているユーザーの中で、さらに配送選択をしたユーザーの 約45% が最短を選択していました。 サービスの特性などにもよるとは思いますが、追加料金を払っても早く商品が届いてほしいユーザーが多いことがわかりました。 まとめ 今回のカートリニューアルはユーザーの 今の課題を解決する に加えて、 あんしんして BUYMA でお買い物を楽しんでもらう というビジョンを持ってサービス開発ができたと思ってます。 プロトタイプを定例で関係者全員に見せてフィードバックをいただけたり、プロジェクトチームの方全員と一丸になれたのもいい思い出です。来年も今年学んだことをベースにさらにいろんなことに挑戦していきたいです。 最後まで読んでいただきありがとうございました。 明日の記事の担当は 情シス の 田中さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、 エニグモ 嘉松です。 この記事は Enigmo Advent Calendar 2021 の17日目の記事です。 はじめに 毎日毎日、それこそ仕事で使わない日は無いくらい、いつもお世話になっている 表計算 ソフト。 昔はみんな Excel を使っていましたが、最近は社内でも Google スプレッドシート を使うことが増えているように感じます。 (因みに私は Lotus 1-2-3 やNumbersは使ったこと無いです。) さて、皆さんいろいろな関数を駆使して作業を効率化していることかと思います。 代表的な関数をいくつか紹介すると、 SUM 表計算 ソフトの関数の代表格といえば何と言ってもSUMですかね。 指定した範囲の合計値を求めてくれます。 「=SUM(A1:A10)」といった形で、合計したいセルの範囲を指定してあげます。 COUNTIF 条件に一致するデータの個数を求めてくれます。 シンタックス は COUNTIF(範囲, 検索条件) です。 「=COUNTIF(A1:A10, "<100")」といった形で、検索の対象とする範囲と、検索する条件を指定します。 結果としては条件に合致したセルの数を返します。 実は私、ほとんど使ったことないですが、よく聞くので紹介しました。 VLOOKUP VLOOKUPを使えると 表計算 ソフトのスキルは中級くらいのレベルになりますかね。 この関数はホントに便利です。もはや無いと生きていけないレベル。 シンタックス は VLOOKUP(検索値, 範囲, 列番号, 検索方法) です。 範囲 で指定した先頭の列を上から順に検索して 検索値 に一致する値を探します。見つかったセルと同じ行の 列番号 にあるセルの値を結果として返します。 検索方法 は FALSE しか使ったことないです。 ちなみに、 Google スプレッドシート の関数の一覧はこちら。 Google スプレッドシートの関数リスト - ドキュメント エディタ ヘルプ 何十、何百の関数が用意されていて、使ったことも無い関数が殆どなのですが、自作のオリジナル関数を作ることも出来ます。 えっ、関数を自分で作る?そんな事できるの?と思われた方も多いかとも思いますが、私も最近知りました(笑) では、これからオリジナル関数を自作する方法を紹介します。 消費税を求めるオリジナル関数を作ってみる オリジナル関数、どう作るか? そのためには Google Apps Scriptで スクリプト を書く必要があります。 Google Apps Script(GAS)とは、 Google が開発・提供している プログラミング言語 です。 JavaScript をベースに開発されたそうです。 スクリプト エディタを起動する Google スプレッドシート を開いたら、タブから「 拡張機能 」>「Apps Script」を押下します。 スクリプト エディタを起動 新しいタブが追加されて、以下のような スクリプト エディタが起動されます。 スクリプト エディタが起動 スクリプト を書く スクリプト エディタが起動されたら、ここにオリジナル関数の スクリプト を書きます。 ここでは消費税を求める関数 myTAX 関数を作ってみます。 /* 消費税を返す関数myTAX */ function myTAX(price){ const taxRate = 0.1; //消費税率 return price * taxRate; } function のあとの myTAX は関数の名前です。 price は、関数の引数です。この関数の引数は1つなので、このように指定します。 const taxRate = 0.1; //消費税率 は taxRate という定数を作成して、税率の0.1(10%)を代入してあげます。 そして return price * taxRate; で引数で指定された price に消費税率 taxRate を掛けて、その結果を return で返してあげます。 スクリプト ができたら、 フロッピーディスク のアイコンをクリックして保存してあげます。 (いまの若い方は フロッピーディスク なんて知らないと思いますが、これでイメージ付くのかな?) オリジナル関数の使い方 では、作った関数を使ってみましょう。 使い方は簡単で、標準の関数と同様に = の後に関数の名前と引数を指定するだけです。 オリジナル関数の使い方 はい、このように消費税の金額が求められました。 オリジナル関数の結果 B2 のセルの右下の小さな四角を下に引っ張れば、 B2 のセルがコピーされて、下の行の消費税が算出されます。 関数のコピー 消費税の算出だけであれば、セルに直接 =A2 * 0.1 とすればできます。 ただ、オリジナル関数を作って消費税を算出するメリットは、消費税の税率が変わった時に スクリプト の消費税率を変えてあげるだけで、変更後の消費税が取得できます。 const taxRate = 0.12; //消費税率 こんな感じですね。 ABテストの信頼度を算出するオリジナル関数を作ってみた 実は、というか当然ですが私は消費税を算出するオリジナル関数を作りたかったわけではありません。 我々は日常的にABテストを実施しています。 例えばクーポンを配布した時に、配布したグループと配布しなかったグループで注文率に差があるが、どの程度差がでるか、クーポンを配布することで注文は増えるけど利益はどうなのか?といったようなことです。 この時、以下のような結果が出たとしましょう。 ABテストではクーポンの配布を実施したグループをトリートメントグループ、クーポンの効果を検証するために敢えてクーポンを配布しないグループをコン トロール グループと呼びます。 対象者 注文者 注文率 トリートメントグループ 179,120 1,677 0.94% コン トロール グループ 11,800 98 0.83% この結果からクーポンを配布したトリートメントグループの注文率が0.94%とコン トロール グループの0.83%を上回っているので「クーポンの効果があった」と判断したくなります。 ただ、世の中、誤差は付き物です。 サイコロを10回振った時に1が10回連続で出たとしても、必ずしもそのサイコロが イカ サマとは決めつけられません。偶然10回連続で1が出る確率は少なからずあります。 同様にクーポンのABテストでも偶然トリートメントグループの注文率が高かった、ということは無くはないです。 そんな事言ったらABテストを行う意味がないではないか、というとそうではありません。 今回の結果が偶然なのか、それとも偶然ではないのかを統計的な手法を使って判断することが可能です。 その方法を統計の用語で検定と呼びます。 検定の方法はいくつかあるのですが、今回は カイ二乗検定 という検定方法を使って検定を行います。 詳細は省略しますが カイ二乗検定 の方法は、以下の順序で行います。 ①実績値を集計 注文あり 注文なし 合計 トリートメントグループ 1,677 177,443 179,120 コン トロール グループ 98 11,702 11,800 合計 1,775 189,145 190,920 ②期待値を算出 注文あり 注文なし 合計 トリートメントグループ 1,665 177,455 179,120 コントールグループ 110 11,690 11,800 合計 1,775 189,145 190,920 ③実績値と期待度数の乖離値を求める ④乖離値を合算して カイ二乗 値を求める ⑤ カイ二乗 値からp値を求める ⑥p値から有意差を求める 一般的に有意差が95%を超えると有意な差がある、つまり偶然ではないと判断します。 このように有意差を求めるには多くの手順が必要となります。 そこでオリジナル関数の出番です。 今回は①〜④の カイ二乗 検値を求めるところまでをオリジナル関数で行い、⑤〜⑥は標準で用意されている関数を使いました。 function myGETCHISQ(a, b, c, d){ // 実績値 var o = [ a - b, b, c - d, d, ]; var s = o[0]+o[1]+o[2]+o[3]; // 期待値 var e = [ (o[0]+o[1]) * (o[0]+o[2]) / s, (o[0]+o[1]) * (o[1]+o[3]) / s, (o[2]+o[3]) * (o[0]+o[2]) / s, (o[2]+o[3]) * (o[1]+o[3]) / s, ]; // カイ二乗検値 var chisq = 0; for (i=0; i<=3; i++){ chisq += (o[i] - e[i]) * (o[i] - e[i]) / e[i]; } return chisq; } この関数の シンタックス は、以下のとおりです。 myGETCHISQ(試行回数A, 注文数A, 試行回数B, 注文数B) 上記の例だと、 =myGETCHISQ(179120, 1665, 11800, 98) となります。もちろん引数にはセルの値を参照させることもできますよ。 この関数で求められた カイ二乗 検値を標準で用意されている CHISQ.DIST.RT 関数に与えてあげることでp値が求められます。 CHISQ.DIST.RT( カイ二乗 値, 自由度) そして、1からp値を引いて100を掛けてあげれば有意差が求まります。 因みにこの例の有意差は75.36%で95%を下回っているので、有意な差が出ているとは言えない、となります。 最後に 今回は Google Apps Script(GAS)を使って Google スプレッドシート のオリジナル関数を作る方法を紹介しました。 また、より実践的な例としてABテストの信頼度を算出するオリジナル関数を作ってみました。 GASを使うと日頃の作業を上手に、そして簡単に効率化することが出来ます。 是非参考にしていただければと思います。 明日の記事の担当はデザイナーの本田さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、Webエンジニアの平井です。 この記事は Enigmo Advent Calendar 2021 の16日目の記事です。 現在、私は BUYMA のSellチームに所属していて出品者関連システムの実装を担当しています。 今季最も注力した受注リストページの Rails 化プロジェクトについて、プロジェクトの概要、プロジェクトを進める上で工夫した点、失敗した点について書きたいと思います。 Rails 化プロジェクトとは もともと BUYMA は PHP で実装されたシステムで、その実装を Ruby on Rails で書き換えるのを弊社では Rails 化と呼んでいます。 Rails 化する目的は以下になります。 PHP の古い フレームワーク で書かれた手続き的なコードを オブジェクト指向 なコードに書き換えて可読性、可用性を上げる。 不要な処理を無くし、レスポンスタイムをあげる。 受注リストページについて 受注リストページは BUYMA で発生した受注が一覧で表示されていて、出品者がその受注情報を確認したり、受注に対して発送通知などの アクションを実施するために用意されたページです。 出品者にとって利用頻度が高いページで、施策で修正する頻度が高いものの PHP の古い フレームワーク で実装されているため修正のコストがかかるのがネックでした。 また、絞り込み機能が乏しく、出品者が目的に沿った取引を絞り込めずに各アクションを実施するのが大変という企画サイドの要望も有りました。 ですので、今回は Rails 化に加えて、検索機能の充実化も開発スコープに含めてプロジェクトを進めました。 システム概要 クライアントサイド クライアントサイドは、実装スピードを上げるため大部分はerbで実装しました。ただ、検索フォームに関しては共通 コンポーネント を利用したかったため Reactを使いました。 サーバーサイド すべて Ruby on Rails です。また、 BUYMA では検索サーバーにSolrを利用していて、この受注リストでも例外なく 受注の検索はSolrを使っています。ただ、旧受注リストではSolrを直接利用していたためSolr特有のパラメーター生成処理など も実装されていました。そこで、検索エンジニアの方に API を開発してもらい新受注リストからはその API を利用して検索処理を実装しています。 API を利用したため、Solrの専門的な知識が不要でアプリケーション側の実装の負担がかなり減りました。 工夫した点 ドキュメントの作成 Rails 化プロジェクトの難しいところは、 PHP で書かれた ソースコード を読み正しく仕様を理解することです。 旧受注リストには、出品者が利用する様々な機能が実装されていますがまとまったドキュメントがなかったので ソースコード を 呼んで仕様を理解する必要があります。 ドキュメントがあれば非エンジニアとのコニュニケーションがスムーズに進むのでこのタイミングでドキュメントを作成しました。 また、実装とドキュメント作成を別々で行うとどうしてもドキュメント作成が後回しになってしまうため、実装しながらドキュメントも同時に作成するように 工夫しました。 ドキュメントを作成した事で、デザイナーやQA担当の方とのコニュニケーションがスムーズに進んだと思います。 フィーチャーフラグの利用 リリースの負担を減らすために、フィーチャーフラグを利用しました。 リリースブランチに受注リスト Rails 化に関わる修正をマージし、該当ブランチを毎週リリースすることで新受注リスト公開時のリリース負担を減らすことが出来ました。 追加機能の優先度付け リリーススケジュールに関しては、12月のセール時期より前に新受注リストをリリースしたいという企画側の要望が有りました。 そこで、企画サイドとコニュニケーションを取り追加機能の優先度を決めて、リリース目標日までに実装すべき機能とそうでない機能を切り分けました。 優先度を決めたのが功を奏し、目標としていたスケジュール日通りに基本的な機能をリリースすることが出来ました。 ユーザーインタビュー 企画的な内容になりますが、企画サイドの担当者の方が、新しく実装する機能やデザインについて多くの出品者に対してインタビューを実施してくれました。 リリース後に追加機能に関してネガティブな意見がほとんどなかったのもこのインタビューのおかげだと思います。 失敗した点 テスト環境でのパフォーマンス監視 そもそも Rails 化プロジェクトの目的としてレスポンスタイムの向上があるのですが、テスト環境でのパフォーマンス監視が不十分だっため 旧受注リストよりも、新受注リストのほうがレスポンスタイムが遅くなってしまいました。 原因としては, N+1とerb上でのrenderメソッドの多用です。 現在、絶賛修正中です。 最後に いかがでしたでしょうか。現在弊社で進められている Rails 化プロジェクトについて書かせて頂きました。 今回プロジェクトで学んだ点を活かし、今後も BUYMA の改善を進めていきたいと思います。 明日の記事の担当は。データ活用推進室の嘉松さんです。 株式会社 エニグモ すべての求人一覧 hrmos.co
この記事は Enigmo Advent Calendar 2021 の15日目の記事です。 はじめに 寒さが身にしみる今日この頃、みなさん如何お過ごしでしょうか。 最近、○○エンジニアという肩書きがよく分からなくなってきたエンジニアの伊藤です。 アドベントカレンダー の時期になると年末になったんだなという実感が湧きますね。 今回は今年一番注力してやってきたMLOpsについて書いていこうかと思います。 はじめに なぜMLOpsなのか? 全体的な流れ 調査 MLOpsの定義、目標の設定 エニグモにおけるMLOps 方針 小さく始めて、大きく育てる 同じ言葉で話せるようにする マネージドサービスの活用 目標 DSが開発しやすい環境作り 開発コスト削減 データの可視化と監視 変化に強い基盤構築 アーキテクチャの設計 ツール選定 アーキテクチャ ML基盤の構築 CI/CD/CTの検討、設計、実装 CI/CD CT おわりに MLOpsとは? については他にも色々と記事があると思うのでここでは触れずに、 導入に向けてどういった手順でどのような取り組みを行ってきたかを中心にご紹介できればと思います。 なぜMLOpsなのか? 弊社でも 機械学習 を活用したプロジェクトがここ最近増え始めています。 そういった中で課題も色々と見えてきました。 属人化 実装内容を把握しているのが個人に依存している レビューがされていない 使用されているバージョンやライブラリがプロジェクトによってバラバラ 開発効率の低下 同じような処理が色々なところにある テストコードがない 心温まる運用 モデルの作成、デプロイは手動 オブザーバビリティの欠如 こういった状況を踏まえつつ、 今後も増えてくるであろう 機械学習 プロジェクトを見据えて、MLOpsを導入することになりました。 全体的な流れ MLOps導入まではざっと下記のような流れで進めました。 調査 MLOpsの定義、方針/目標の設定 アーキテクチャ の設計 ML基盤の構築 開発体制やルール等の策定 実装 CI/CD/CTの検討、設計、実装 監視周りの検討、設定 4以降についてはきっちり分けて進めていた訳ではなく、走りながら(実装、構築しながら)並行で進めていくことが多かったです。 以降で各項目からいくつかピックアップして詳細については記載します。 調査 私自身、MLOpsについては何となく分かっていたつもりでしたが、先人たち(先行他社)の取り組みや関連資料を読み漁りました。 ここで特に気を付けていたのは下記の点です。 機械学習 プロジェクトにまつわる課題としてはどういったものがあるのか? 現時点で弊社としての課題感はないが、今後課題になりうるものがないか? 先人たちの取り組みを真似るのではなく参考にする 各課題に対してどういった考えで、どのようなアプローチをとっているのか 個人的な考えですが、組織の規模感や文化といったものが違うのに同じような取り組みをしても失敗すると思っています MLOpsの定義、目標の設定 MLOpsは バズワード だなんて意見もあります。 MLOpsという単語自体が一人歩きしている感もあるかなと思います。 なので一通り調査が終わったあとは弊社におけるMLOpsを定義することにしました。 今更ながら弊社のメンバー構成を紹介すると下記の通りで、スモールスタートにはちょうど良い人数かなという印象です。 データサイエンティスト(以降、DS) ✕ 2名 エンジニア(以降、 Ops ) ✕ 2名 エニグモ におけるMLOps 「DS(ML)とエンジニア( Ops )がお互いの役割を理解し、強調し合うことで、 機械学習 モデルの実装から運用までのライフサイクルを円滑に進めるための仕組みづくりやその考え方」 方針 続いて方針についてです。3つ挙げました。 小さく始めて、大きく育てる 同じ言葉で話せるようにする マネージドサービスの活用 小さく始めて、大きく育てる 最初からあれも、これもと色々なことを詰め込みすぎない いつまで立っても設計が固まらず、システムの構築や運用が始まらないというのは一番避けたい MLOpsを取り巻く環境はまだまだ成熟しておらず、ノウハウや技術も発展途上の段階 柔軟性が高くスケールする基盤であることを第一としてシステムを設計、構築する 同じ言葉で話せるようにする スキルセットやメンタルモデルが異なるメンバー同士で作業を進めていく上ではどうしてもコニュニケーションコストは高くなる エンジニア同士では 暗黙知 としているようなルールやお作法についてもきちんと明文化し、使用するツールや フレームワーク なども共 通化 ガチガチのルールで縛るのは好きではないので、バランスには注意 マネージドサービスの活用 限られた人的リソースの中で対応していくにはどうしても人が ボトルネック になりがち&人的コストはスケールさせるのが難しい お金で解決できるようなところは積極的に利用する 何でもかんでもマネージドという訳ではなくバランスが大事 コスト(人とお金) X システムとしての柔軟性は意識する 目標 最後に具体的に何をしていくか、どこを目指すかの目標についてです。 ひとまず今期(2021年)としては下記を目標として挙げました。 DSが開発しやすい環境作り UIベースでの実験管理 再現性の確保(今期はデータではなく環境面にフォーカス) パイプラインのバージョン管理、ライブラリ、 フレームワーク の統一 各種ルール化(明文化) コーディング規約、レビュー規則、ログ仕様、開発フロー 実験段階から本番環境へのデプロイがシームレスに可能 ドキュメントの充実化 開発コスト削減 共 通化 できるものは共 通化 し、再利用可能な仕組みづくり( 車輪の再発明 はしない) データの可視化と監視 モデルの性能指標など、可視化して確認したいデータが見たい時に見られる状態 過不足のない監視、アラート 変化に強い基盤構築 汎用性が高く、スケール可能な基盤であること チームとして1つの目標を目指す上で、言葉の定義や、目標の明文化というのはとても大事だと思っているので、 この辺については特に丁寧に行いました。 また目標としてやるべきことを明確化することと合わせて、やらないこともあえて決めておきました。 どうしても作業を進めていく中で「こんなこともできるとうれしいよね」みたいなことは出てきます。 方針にも挙げていますが、まずは小さく始めることを第一としていたので、こういった迷うケースでも チームとしてブレずに進めたのは良かったなと思います。 アーキテクチャ の設計 続いて アーキテクチャ の設計についてです。 ツール選定 アーキテクチャ を設計する上ではまずはMLワークフローの中心となるツール選定を行いました。 MLOpsを取り巻くツールは MLOps Toys に集約されるようにとても多岐に渡ります。 この1つ1つを調査、検証していてはとても時間がかかるのでツール選定については時間をかけずに、 下記の観点で一番相性の良さそうなKubeflowを選択しました。 利用事例が多い 開発が活発 柔軟性がある まずはPipelinesのみ導入し、その後必要に応じて他の コンポーネント 導入が可能 Kubernetes の構築、運用実績がある アーキテクチャ 分析等に扱うデータはBigQueryに集約している関係で、そのデータを扱うML系のプロジェクトも GCP 上で構築することが多いです。 そのため今回新たに構築するML基盤についても GCP 上で構築しています。 またKubeflow PipelinesについてはそのマネージドサービスであるAI Platform Pipelinesを利用することとしました。 ML基盤としてGKEを利用 3パターンのnodeプールを用意 KFP nodeプール Kubeflow Pipelinesが動作するための コンポーネント 群専用 ML nodeプール MLパイプラインでの処理を実行するpod用のnodeプール 0 to Nのノードスケール 使用するマシンタイプは短時間で高速で処理が完了するようにCPU,メモリともに潤沢なものを選択 API nodeプール 予測結果のサービング用 API 向け 1 to Nのノードスケール 使用するマシンタイプはCPUメインの比較的軽めのものを選択 GCP マネージドサービスとして下記を利用 AI Platform Pipelines BigQuery Cloud Storage Cloud Datastore Container Registry 各種メトリクスの可視化、監視にはDatadogを利用 ML基盤の構築 ML基盤としては上記の通り、GKEをインフラとしつつ、AI Platform Pipelinesを利用しています。 基本的には全てIaCとしてterraformで管理、運用していますが、AI Platform Pipelinesは未対応だったため対象外として扱っています。 また、システムに名前があった方が良いよねということで、 メンバーそれぞれで候補となる名前をいくつか挙げて、投票で一番多かった Capella という名前に決まりました。 CI/CD/CTの検討、設計、実装 CI、CD、CTともに全てGitlab CIを利用するようにしました。 Cloud Buildなども検討しましたが、Gitlab CIの方がより柔軟性が高かったのと、元々別のシステムでもGitlab CIを利用していたというのが理由です。 CI/CD テスト環境 本番環境 ポイント 各工程の細かい処理については置いておいて、ポイントについてここでは記載します。 成果物はテスト、本番環境で共通 Kubeflow Pipelinesにおける成果物してはパイプライン( コンパイル 済みファイル)とそこで実行される コンポーネント のコンテナイメージとなります。 本番環境ではテスト環境で作成された成果物を使用してパイプラインのデプロイを行います。 integrationテストとして実際にKubeflow Pipelines上で コンポーネント の動作検証を自動テスト 作業を進める中でコンテナイメージ単体としては問題なく動作するが、Kubeflow Pipeline上でパイプラインとして実行した際に問題となるケースがいくつかあったため組み込みました。 CT パイプラインの実行トリガーをどこに持たせるかは正直一番悩んだところです。 Kubeflow Pipelines側に標準実装されているスケジュール機能を利用するのが良いのかなとも思いましたが、下記の理由からGitlabパイプラインスケジュールを利用して定期実行するようにしています。 CI/CD用に作成したツールを使い回せる gitブランチと紐付けてスケジュール実行が可能 master → テスト環境 production → 本番環境 CI/CDジョブとシームレスに扱うことができる CI/CD/CTがすべてGitlabで管理されているという分かりやすさ Kubeflow Pipelines側のスケジュール機能で実行した場合に、MLパイプラインの実行ステータスの通知をどうするかの検討が別途必要だった Gitlab側であればパイプラインステータスを判断してSlack連携が可能なことは確認済みだった なお、下記がGitlabパイプラインスケジュールのUIになります。 内容としても直感的に分かりやすく、cronジョブベースでの定期実行と、設定した変数で処理が分岐できるようにしています。 おわりに ここまで調査/検討から始まり導入までについて、ざっとご紹介させていただきました。 書ききれていない内容もありますが、少しでもどういった取り組みをしているのかが伝われば幸いです。 また今回構築したシステムについてはまだまだ未熟で、これからどう成長させていくかが大事かなと思っております。 そんな訳で エニグモ ではMLプロジェクトを一緒に盛り上げていくメンバーについても募集中です! この記事を見て少しでも興味を持っていただけたら、まずは雑談からでもOKですので気軽にご相談ください。 明日の記事はエンジニアの平井くんです!きっとワクワクするような内容だと思うのでめちゃくちゃ楽しみですね!!! 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは。サーバーサイドエンジニアの伊藤です。 この記事は Enigmo Advent Calendar 2021 の 14 日目の記事です。 みなさんは シェルスクリプト を実装する機会はどのくらいの頻度でありますでしょうか? 私は社内ツールや個人で利用するちょっとしたツールを作成する際に、 シェルスクリプト を実装することがあります。 とはいえ、普段の業務では Ruby on Rails を用いて実装をすることが多いので、 シェルスクリプト を実装する機会自体はそれほど多いものではありません。 その為、 シェルスクリプト 実装時に割と基本的な罠に陥ってしまうことがありました。  そこでこの記事では シェルスクリプト の実装に潜む 4 つの罠を共有したいと思います。 1. shebang の記載漏れ 2. 適切な set options を指定しない -e (errexit) -u (nounset) 3. 1行で宣言と代入を行う 4. Double quotes で変数を囲わない shellcheck とは? 参考 1. shebang の記載漏れ # bad echo " FOO " # good #!/usr/bin/env bash echo " FOO " ファイルの 1 行目に shebang を記載することで、プログラムを実行するシェルを明示的に指定することが推奨されます。 というのも、シェルによってはサポートされている機能が異なることがあるため、 スクリプト を実行するシェルを明示的に指定することで意図しない挙動を避けることができます。 ここで 1 つの疑問が発生しました。 シェルスクリプト において shebang の記述は下記の通り複数ありますが、好ましい記述方はどれなのでしょうか? #!/bin/bash #!/usr/bin/env bash 一般的には #!/usr/bin/env bash の形式を利用することが推奨されるようです。 絶対パス 等で指定をした場合、実行環境によっては指定の場所に実行可能なコマンドが存在しない可能性が考えられます。 上記の方法で記載すると、 $PATH から実行可能なコマンド(この場合 bash )を捜査してくれるのでポータビリティという点でメリットがあります。 さて、少し話が逸れてしまったので 閑話休題 、 2 つ目の罠に話を移します。 2. 適切な set options を指定しない # bad #!/usr/bin/env bash echo " FOO " # good #!/usr/bin/env bash set -eu echo " FOO " -e (errexit) こちらのオプションを指定することで、 シェルスクリプト の途中でエラーが発生した場合その場で直ちに スクリプト を終了します。 大抵のケースではエラーが発生した場合、その場で スクリプト を終了したいことが多いと思いますので有効にしておくことが推奨されます。 ちなみに、エラーを許容したい場合には下記のように記述することで該当のコマンドのみエラーを許容することが可能です。 command1 || true echo " This line is executed anyway " -u (nounset) こちらは変数宣言に関するオプションです。 デフォルトでは宣言していない変数を利用した場合、空文字として扱われてしまいます。 その為、 typo 等で未宣言の変数を利用したとしても特にエラーにはならないので、意図しない挙動を招くことがあります。 そういったケースを避けるためこちらのオプションを有効にしておくことをお勧めします。 3. 1行で宣言と代入を行う # bad export BAR= " $( command2 ) " # good BAR = " $( command2 ) " export BAR 上記の スクリプト では command2 の結果が無視されてしまいます。 set -e を指定している場合、本来エラーが発生した箇所で スクリプト は終了します。 ただ、上記のケースでは command2 の結果に関わらず後続の処理が実行されてしまいます。 その為、代入と宣言の箇所を分けることが推奨されます。 4. Double quotes で変数を囲わない # bad file = " sample file.txt " ls $file DIR = " $( dirname $0) /.. " # good file = " sample file.txt " ls " $file " DIR = " $( dirname " $0 " ) /.. " double quotes を利用しないと該当の文字列中にスペースやタブ等を含むケースで、意図しない結果を招くことがあります。 例えば、上記のようにファイル名にスペースを含むケースだと、double quotes で該当の変数を囲わない場合スペースによって sample と file.txt という 2 つの単語に分割されてしまいます。 結果として、 sample file.txt というファイルではなく sample と file.txt を引数に ls コマンドが実行されてしまいます。 このような、意図しない挙動を避ける為、 シェルスクリプト では変数を double quotes で囲むことが推奨されます。 さて、ここまで シェルスクリプト の実装に潜む 4 つの罠について記載してきました。 シェルスクリプト を実装したことがある開発者にとっては、これらのどれもが一度は経験してきた問題なのではないでしょうか? これらの問題が一般的ではある一方で、全てをマニュアルで人間が気をつけることは難しく労力を要する作業かと思います。 ということで、 shellcheck というツールを利用することをお勧めします。 shellcheck とは? シェルスクリプト 用の静的 Lint ツールです。 こちらを利用することで、上記で説明してきたエラー(罠)を簡単に検知することが可能です。 コマンドを実行すると、下記の通り問題のある箇所を明瞭に指摘してくれます。 >>> shellcheck ./tmp.sh In ./tmp.sh line 1: set -eu ^-- SC2148 ( error ) : Tips depend on target shell and yours is unknown. Add a shebang or a ' shell ' directive. In ./tmp.sh line 5: echo ${baz} ^----^ SC2086 ( info ) : Double quote to prevent globbing and word splitting. Did you mean: echo " ${baz} " For more information: https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y... https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... さて、これだけでも記憶を頼りにマニュアルで全てを確認することに比べ、 簡便になったとは思います。 しかし、こちらのコマンドを修正の度に毎度手動実行することは非常に骨の折れる作業であり、非人間的です。 ということで、CI として設定して自動で実行しましょう。 今回はサンプルとして昨年の Advent Calendar 2020 の記事に記載した自作の OATHTOOL ラッパーコマンド に shellcheck を CI として追加してみます。 こちらのレポジトリは GitHub 上に存在するので、今回は GitHub Actions を用いて設定していきます。 設定ファイルは下記。 # .github/workflows/main.yml name : CI on : push : branches : [ main ] pull_request : branches : [ main ] jobs : shellcheck : name : Shellcheck runs-on : ubuntu-latest steps : - uses : actions/checkout@v2 - name : Run ShellCheck uses : ludeeus/action-shellcheck@master with : scandir : './bin' shellcheck は既に GitHub Action としてマーケットプレースに存在するので、設定自体は簡単です。 上記のように設定ファイルを 1 つ追加するだけで設定は完了です。 github.com 無事に CI として shellcheck が走るようになりました。 最後まで読んでいただきありがとうございました。 明日の記事の担当は検索エンジニアの伊藤 明大さんです。お楽しみに。 参考 What is the preferred Bash shebang? (最終アクセス: 2021/12/05) set man page (最終アクセス: 2021/12/05) koalaman/shellcheck (最終アクセス: 2021/12/05) 株式会社 エニグモ すべての求人一覧 hrmos.co
エンジニアの木村です。この記事は Enigmo Advent Calendar 2021 の 13日目の記事です。 いろいろやってますが、BUYチームという購入UXに関わる機能開発を担当するチームのマネージャーもやっています。11月末に行われる ブラックフライデー 、 サイバーマンデー といった大型キャンペーンに備えた開発もそのチームで担当したのですが、今日はそれに備えて行った負荷対策の1つの取り組みについてお話しします。 悔しい思いをした去年の ブラックフライデー 平素より BUYMA をご利用いただきまして、誠にありがとうございます。 本日0時頃より、サイトに繋がりづらい状況が続いており、現在復旧に向けて全力で対応を行っております。 ご不便をお掛けしてしまい大変申し訳ございませんが、何卒ご理解を賜りますようお願い申し上げます。 — BUYMA (バイマ)| 海外ファッション通販サイト (@ BUYMA ) November 26, 2020 これは昨年の ブラックフライデー 開始直後の BUYMA オフィシャルアカウントのツイートになります。。悲しいですね。下のリンクから当時の BUYMA の状況を表すタイムラインが見れますが、どういう状況だったか推察いただけると思います。 バイマ OR buyma until:2020-11-27_00:50:00_JST -filter:links -filter:replies - Twitter Search ユーザーのログイン状態管理が重い 原因の1つは、ユーザーのログイン状態の管理をサービスのメインDBで行っているところでした。スケールが難しい RDB です。最近はアクセス集中時にユーザーのログイン済み比率も増え、そのユーザーのアクセスでは毎リク エス トそのDBのテーブルへSELECTが走るのでメインDBが高負荷になり、メインDBなのであらゆる機能へ影響するためサイトとしては繋がりにくくなるという状況でした。もちろん、以前から問題視されていた仕組みではありましたが、ログイン状態の管理という重要な機能でもあり、原作者もおらず、別の仕組みへの移行の難易度が高くずっと放置してしまっていました。 したがって、DBのリソースをログイン管理のために温存する必要があるため、動的コンテンツ(HTMLや API )配信にも CDN を入れてキャッシュしたり、DB負荷が高い機能はアクセス集中が予測される時間帯には停止するようにスケジュールしたりなどいろんな手を尽くしていま。しかし、それでも ブラックフライデー だけは耐えることができませんでした。 セッションの保存を RDB からredisへ そこで、ログイン状態の保存先を RDB から、より読み取り レイテンシー が小さく、読み取りの集中にも耐久性のあるRedisへと移行するプロジェクトが始まりました。その移行の際に行った工夫が記事の本題となります。この工夫が、たまたま後日読んだこちらの本で紹介されている移行パターンそのものだったのでこちらの本の用語を使って説明していきます。 モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド 作者: Sam Newman オライリージャパン Amazon 利用パターン1:抽象化によるブランチ 現代のソフトウェア開発では1つのコードベースでブランチを切って複数人で並行して行うのは普通ですが、あまりブランチを切ってから長い時間が経つと差分が大きく、レビューやマージが大変になります。そこで、実装を改良しようとしている機能の抽象を作り、既存の実装と並行してその抽象の新しい実装を別に作り、さらにあとで切り替えるという方法が 抽象化によるブランチ と呼ばれる移行パターンです。大きな変更であっても、寿命の長いブランチを作らずに、他で並行して行われる開発と影響しにくいように開発が可能です。今回の事例で1ステップずつ手順を説明していきます。 今回は抽象というよりは ファサード となるクラスを前に立てて新実装へ切り替えているので、 モノリス 内での スト ラングラー パターン と言っても良いかもしれません。 ステップ0:変更前 変更前のアプリケーションはこのような構成でした。 ActiveRecord のモデルを介してDBのテーブルを呼び出すというオーソドックスな作りかと思います。 ステップ1: ファサード を作成し、既存のクライアントの向き先を変更 最初に行ったのは、実装の変更を呼び出し元へ影響するのを抑えるため、ログインステータスの操作を抽象化するために ファサード となるクラスを設けたことです。また、この ファサード を利用するように、順次呼び出し元の向け先もこの ファサード へ切り替えました。 ステップ2:並行して新しい実装を作成 従来の実装を稼働させつつ、新しい実装を横に作成します。実装のソースはデプロイされますが、この段階では事実上、従来の実装だけが動いたままです。 ステップ3: ファサード 内で新しい実装を使うように切り替え ファサード 内部を変更して、処理の流れを従来の実装から新しい実装へと切り替えます。この切り替えは後述する 同時実行パターン や カナリア リリース を使って段階的に行いました。 利用パターン2:同時実行 新旧の実装を切り替える時に、それぞれの実装で処理結果が変わらないことを担保する必要があるのですが、通常、デプロイ前にテストを頑張るという手段になると思います。ただ、テスト尽くしても本番環境で起こりうるシナリオは全て網羅することは困難です。特に、今回の変更はユーザーのログイン状態の管理になるので、不具合があると誤ってユーザーをログアウトさせてしまったり、あるいはBANしたユーザーを誤ってログインさせてしまったりなど重大な問題となってしまいます。 そこで、新実装を本番へ出してしまい、旧実装と同時に新実装も呼び出しつつ、旧実装の結果を呼び出し元へは返しておきながら、それぞれの実行結果を比較して新実装の結果が信頼できるか検証するという 同時実行 というパターンを利用しました。こちらの図のとおり、 ファサード 側でDB、Redisそれぞれの結果を処理が通るたびに比較検証し、差分があればログに出しておくようにしました。また、 ファサード の処理のタイミング以外でもバッチで定期的にそれぞれの内容をスキャンし、差分有無の検証も行いました。差分が出れば原因を調査し、原因を潰していくという作業を差分が出なくなるまで続けました。 カナリア リリースも利用 最初に同時実行モードに入る前に、気になったのはRedisの負荷でした。ほかの機能で導入実績はありましたし、負荷テストはしていたものの、やはり本番の トラフィック を一気に向けるのは勇気が要りました。そこで、徐々に10%ずつ新実装へ トラフィック を向けるように カナリア リリースを行いました。また、同時実行により十分に検証を行い、新しい実装へと切り替えるタイミングでも、予期しないユーザーへの影響の可能性を考え、そのタイミングでも10%ずつ新実装のみの結果を利用するようにしています。 カナリア リリースの仕組みは、 cookie ベースで トラフィック を振り分ける既存のABテストの仕組みを利用しています。 心穏やかに移行完了 以上の工夫によりあまりドキドキすることなく、心穏やかに移行を終えることができました。数ヶ月ほど時間はかかりましたが、抽象化ブランチの仕組みにより、切ってから長時間経った差分の大きいブランチをマージする必要も無かったですし、同時実行によりバグを十分出し切った上で、しかも カナリア リリースで少しずつリリースできたためです。時間がかかるというデメリットはあったものの、特に品質に気を遣う変更には有用かと思います。 負荷対策の効果 ついに迎えた ブラックフライデー 当日ですが、サイトのパフォーマンスを落とすことなく乗り切ることができました。こちらは、開始直後11/26の0時台の企画メンバーが集うslackのチャンネルの様子です。例年なら不安定になるはずのサイトがサクサク動いていて盛り上がっていました。 もちろん、今回紹介した移行以外にも負荷対策や様々なパフォーマンス改善策を重ねた結果なのですが、それらについてはまた別の機会に紹介しようと思います。
この記事は Enigmo Advent Calendar 2021 の12日目の記事です。 こんにちは。 BUYMA の検索やMLOps基盤周りを担当している竹田です。 この一年間はTerraformを業務で利用することが多かったため、普段気を付けていることなどを運用tipsとして紹介したいと思います。 Terraform Terraformは言わずと知れた Infrastructure as Code (IaC) を実現するためのツールです。 先日v1 🎉 になり、安定してきた印象があります。 Terraformに限った話ではありませんが、コード管理されている安心感は何ものにも変えがたいものがあります。 本記事はTerraform自体の説明は割愛し、ある程度利用したことのある方を対象としています。 個人の主観を多分に含んでいる、およびGoogleCloudに寄った内容のため他 クラウド ベンダーとは若干記述の毛色が違うといった可能性があります。あくまで参考程度としていただけると幸いです。 また、 terraform Advent Calender もあるようですので、より多くの情報を得たいという場合はそちらも参考にしていただくと良いかと思います。 定期的なバージョンアップ Terraformはv1化により安定してきたように思いますが、 クラウド ベンターのproviderは変化のサイクルが早く、新機能の追加やベータ版のGA化など定期的に追従した方が良いです。 気付かない内に有用な機能がリリースされており、無効の状態で運用を継続している可能性がある 新機能があった場合に気付きを得られる 項目の定義方法が変更になっている場合がある 設定理由を残す インフラ面のライフサイクルとして定義変更の頻度が少ないこともあり、いざ定義を変更しようとした際に設定理由を忘れていることが多々あります。 「この機能は何々の理由で有効・無効にした」といった内容を何かしら文章として残しておくことをお勧めします。 terraform apply の際にエラーとなってしまう場合がある 主に認証周りや、providerサイドで項目のvalidationがされていないものについて発生することがあります。 terraform plan した際には正常終了するものの、いざ terraform apply するとエラーになるというものです。 すでにコード管理上にcommitしていると二度手間になるため、開発環境やテスト可能な環境で terraform apply まで実施しておくことをお勧めします。 書き方のtips tfファイルの記法は若干クセがあるため、どの書き方が適切なのか、どう書くとやりたいことを実現できるのか分からない状況に遭遇することが稀にあります。 以下に一部例を挙げてみます。 変数参照 ハッシュ記述 value["preemptible"] なのかシンボル的記述 value.preemptible なのか迷います。大抵はどちらでも問題ありません。 ただし、混在すると見にくくなったり、記述箇所によっては意図があるように感じたりする場合があるため、統一感を持たせた方が良いように思います。 ↓こんな混在した書き方ができてしまう node_config { preemptible = each.value["preemptible"] machine_type = each.value.machine_type disk_type = each.value["disk_type"] oauth_scopes = each.value.oauth_scopes dynamicブロック 環境毎にリソース内の定義有無を設定できるため非常に便利なdynamicブロックですが、リソースによっては後で削除できないパターンが存在します。 例えば GoogleCloud のノードプールに設定可能なtaint定義などが該当します。taintの説明は こちら を参照ください。 なお、ここで例として挙げているtaintの変更は、変更の際にノードプール自体が作り直されてしまいますのでご注意ください。 taint定義を以下のようにdynamicブロック定義していたとします。 dynamic "taint" { for_each = contains(keys(local.gke_nodepools), "taints") ? local.gke_nodepools.taints : [] content { key = taint.value.key value = taint.value.value effect = taint.value.effect } } variables は以下のようになっており、 terraform apply により実環境にtaintが設定されていたとします。 locals { gke_nodepools = { taints = [ { key = "testkey" value = "testvalue" effect = "NO_SCHEDULE" } ] } あるタイミングでtaint定義が不要となり、削除したいと考えて variables の taints 項目自体を除去して terraform plan を実施します。すると No changes. Infrastructure is up-to-date. 削除差分が出ると思っていたのに差分が一切検出されませんでした 😢 これはdynamicブロック対象のオブジェクトを定義しない場合に、そもそも定義が存在しないものと解釈されて空の内容では更新してくれないためです。 こういった場合は、taint定義を以下のようにします。 taint = [ for item in local.gke_nodepools.taints : { key = item.key value = item.value effect = item.effect } ] 続いて、 variables を空に設定します。 locals { gke_nodepools = { taints = [] } このように定義することで、taint定義を空で更新することが可能となります。 以下は terraform plan 時の差分結果です。 ~ taint = [ # forces replacement - { - effect = "NO_SCHEDULE" - key = "testkey" - value = "testvalue" }, ] 最後に システム規模としては小〜中規模のため、規模面で困ったことは今のところありません。 大規模システムでは自動化などを考慮する必要がありそうなので、その辺りの悩みがない分は楽かもしれませんね。 今後はTerraform以外にもIaC関連のツールを利用するかもしれませんが、しっかり追従していきたいなと思います! 明日の記事担当はエンジニアマネージャーの木村さんです。お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、 エニグモ カスタマー マーケティング 事業本部で出品審査などを担当している杉山です。この記事は Enigmo Advent Calendar 2021 の10日目の記事です。 昨年の アドベントカレンダー では、 日頃の担当業務についてWantedlyで書きました が、今回は開発者ブログにお邪魔しました。 エンジニアではなくビジネスサイドの人間ですが、通常業務の傍らITツールを駆使して、ユーザー対応の現場の自動化や効率化などに取り組んでいます。 どうしても手元で作業が必要で、 エンジニアに開発してもらうほどではないんだけど、これが自動になったら楽なのに!とかもっと効率よく日々の業務をこなして価値の高い仕事に取り組みたいよー! と思う瞬間がありませんか?私はあります。もう毎日のようにあります。 もちろん大規模な運用改善やシステムの導入などはエンジニアと一緒に開発したほうが圧倒的にいいという案件も多々あるので見極めが必要ですが、ひとまず現場の人間が(ほぼ)ノーコードでいろいろできると、エンジニアのリソースが空くまでのその場を凌いだり、新しいことについて考えて取り組む余裕を作り出すことができます。今回はそんなお話の一つです。 餅は餅屋ですから専門家から見れば拙いやり方をしている面もあるかとは思いますが、そこは何卒多めに見ていただければ幸い。 NPSアンケートのチェック運用をツールの自動連携で効率化する 本日のテーマは、BigQueryのデータをデイリーでAirtableに自動連携して、 BUYMA のNPSアンケートチェック業務の効率化をした話を書きたいと思います。 NPS(ネットプロモータースコア) とは、EC事業者にとっては定番ですが、取引が完了したお客様に任意で答えていただいている、 BUYMA の顧客のロイヤリティを計測するためのアンケート調査です。 NPSの回答には、 BUYMA や出品者様のお取引についての貴重なご意見が詰まっています。 エニグモ ではNPS専用のプロジェクトチームを部門横断で結成し、回答を全てチェック、 アクティブサポート や今後の開発・施策の検討に活用させていただいています。 これまでのNPSチェック運用と課題 NPSプロジェクトでは、毎日いただく数百のアンケートすべてに目を通し、気になるものをピックアップして定期 MTG で今後の対応を議論しています。チェック作業は曜日ごとにプロジェクトメンバーで分担して行っているのですが、それがなかなか手間のかかる作業となっており、また、チェック対応結果の活用という点でも課題が多くありました。 いままでのやり方 Gmail にデイリーで送られる回答データを確認し、 Gmail 上に対応ログを残す Gmail で送られるNPSアンケートのサマリ例 課題 チェックに時間がかかりすぎる データに直接チェック結果を書き込むことができないので、気になったポイントなどをメモする場合 コピペしてドキュメントを整形する作業を毎回する必要 がある ピックアップの MTG で決まった対応について、 進捗管理 がしづらい 議論のログがメールにしかないため、 対応担当者が手元で別途タスクリストを作ったり情報をメモしておかないといけない アンケートに答えていただいた方へご連絡する際に、メールリストの作成をするだけで一苦労 手動で再度データを抽出し対応の一覧を Excel で作成、そこからメール送信対象者をチェックして SQL でメールアドレスリストを抽出……と 複雑な手順が必要 だった どのアンケートについてどういった議論が起こり、どんな対応を行ったのか、あとから振り返る手段が限られている Gmail の履歴から検索してひとつひとつ中身を見るしかないため、 一覧性が低い どんなご意見がどのくらいの件数来ているのかなどを 定量 的に把握することができない ……といった具合に、NPSアンケートを導入した初期から続く古の運用のため、やりづらいポイントが多数ある状態でした。 チェック運用のツールを Gmail →Airtableに移行し効率化 そこで、以下の2点について解決すべく、運用改善を行いました。 アンケートのチェック作業の手間を減らす 今後の対応のために過去のデータを参照しやすくする 購入者様がアンケートに回答→メンバーが内容チェック→対応内容を決定しタスクリストを作成→ 進捗管理 →回答者様へのご連絡→過去のピックアップ内容の振り返り 、という一連の業務の流れを整理し直して、最小限の手数で作業が完了できるようにします。そのために、チェック作業のツールを Gmail からデータベースサービスAirtableに移行することにしました。 Airtableは、一言で言うときれいで賢い スプレッドシート です。 表形式のデータを直感的に、かつ自由度高く扱うことができる ため、多数のデータをチェックして分類したり、追加の情報を手動でメモして管理したり、ということが Google Spreadsheet や Excel 以上に簡単に見栄え良くできます。 今回はBigQueryから抽出したNPSアンケートのデータを自動でAirtableに同期し、そこでピックアップ運用を全て完結させるようにしました。やることは単にデータの自動同期なのですが、使用ツールが変わるとチェック作業は劇的にやりやすくなります。 ※ 今回の記事ではAirtableの細かい使い方については割愛しますが、使いこなせれば本当に便利すぎるサービスなので、おすすめです! ▼参考記事 脱 Excel ・ スプレッドシート !WebプロジェクトのためのAirtable活用術 loftwork.com 使用ツールと自動化の流れ 今回の自動化の流れと使用したツールは以下になります。 Google BigQuery NPSアンケートの回答データが溜まっているDB Google App Script(GAS) BigQueryから毎日自動でデータ抽出し スプレッドシート に転記するプログラムを作成 Google Spreadsheet GASで抽出したデータを一時的に溜め、Zapierのトリガーにするために使用 Zapier 複数の Webサービス を組み合わせて独自の自動ワークフローを作成できるタスク自動化ツール。Spreadsheetに同期したデータをトリガーにして、Airtableに自動転記する設定を作成 zapier.com Aitable 自動同期したデータの目視チェック、対応ログの記録などを行う airtable.com GASでBigQueryからデータ抽出してシートに記入 GASを使ってBigQuery上で SQL を動かし、 スプレッドシート にデータを転記するまでのコードは以下のような感じです。これは社内の他のGASやググった内容を参考にあれやこれやしてなんとか作りました。GAS、もう数年間書いては忘れを繰り返しているのですが、今回の対応でやっと結構定着した気がします……。 GASは深夜の3時に毎日起動し前日分を取得するようトリガー設定をしています。 //BigQuery var projectNumber = 'データ抽出元の任意のプロジェクトNo' ; //スプレッドシート var ss = SpreadsheetApp.getActiveSpreadsheet(); //SQLの結果を出力するシート var sheetNPS = ss.getSheetByName( '任意のシート名' ); function check() { //処理開始メッセージ Browser.msgBox( "処理をしています。しばらくお待ちください。" ) //SQL結果書き出しシートのクリア sheetNPS.getRange(2, 1,sheetNPS.getLastRow(),sheetNPS.getLastColumn()).clearContent(); // SQLを生成 var sql = "任意のクエリ" ; Logger.log(sql); // SQLを実行する準備 var query_results; var resource = { query : sql, timeoutMs: 1000000, // Standard SQLを使用する場合はLegacySqlの使用をfalseにする useLegacySql: false } ; try { // SQLを実行 query_results = BigQuery.Jobs.query(resource,projectNumber); } // エラーが発生したらログ出力、メッセージ出力して終了 catch (err) { Logger.log(err); Browser.msgBox(err); return ; } while (query_results.getJobComplete() == false ) { try { query_results = BigQuery.Jobs.getQueryResults(projectNumber,query_Results.getJobReference().getJobId()); if (query_results.getJobComplete() == false ) { Utilities.sleep(3000); //以下の謎のエラーがでたら、この値を増やすか、timeoutMsの値を増やす。「ReferenceError: 「query_Results」が定義されていません。」 } } catch (err) { Logger.log(err); Browser.msgBox(err); return ; } } Logger.log(query_results); var resultCount = query_results.getTotalRows(); var resultValues = new Array (resultCount); var tableRows = query_results.getRows(); // 抽出結果を配列(resultValues)に格納 for ( var i = 0; i < tableRows.length; i++) { var cols = tableRows [ i ] .getF(); resultValues [ i ] = new Array (cols.length); for ( var j = 0; j < cols.length; j++) { resultValues [ i ][ j ] = cols [ j ] .getV(); } } // 配列(resultValues)の内容をシートに出力 sheetNPS.getRange(2,1,resultCount,tableRows [ 0 ] .getF().length).setValues(resultValues); Browser.msgBox( "完了したよ!" ) //完了メッセージ Zapierの設定 次に、 スプレッドシート のデータ更新をトリガとしてAirtableにデータを転記するZapierを設定します。 当初はGASでそのままAirtableに転記することを想定して作り始めたのですが、Airtableの API Documentをよくよく見ると、 一度に API で追加できるレコードは10まで、という制限 があり、数百行を一気に追加したかったため間にZapを挟むことにしました。 トリガに スプレッドシート のレコードの追加もしくはアップデートを指定 先ほど作ったGASのシートを指定 1行1トリガとして先ほど抽出したデータがテストに出てきます これでトリガー設定は完了です。 次に、Airtableに転記をするための設定をします。 APPにAirtableを選択しアカウントを連携 Create Recordを選択 あらかじめ作成したAirtable上のカラムと スプレッドシート のデータの対応を設定 テストで入力内容を確認し、問題がなければZapをONにする データを同期したAirtableシート これで、毎日前日分のデータをAirtableに自動同期することができます!! Zapierのトリガ数の上限を解除する と、うまくいったかと思いきや、運用開始してみたら問題が発生。 Zapierは動作したトリガ数に応じて課金されていく仕組みなので、 誤作動防止のために一度に動くトリガが100件を超えると自動でストップする仕様 になっていました。今回作ったZapは毎日数百件ある更新データをひとつひとつトリガとしてZapを動かす設定のため、動かすたびに毎回Zapが止まってしまいました。一時停止したZapはボタンひとつで再開作業をすれば問題なく動くのですが、毎朝対応が必要になるのでこれでは自動化の良さが半減してしまいます。 最初は解決方法がわからず、毎日再開をするひと手間をしばらく続けていたのですが、やっぱりめんどくさい!! Zapierに問い合わせてみると、トリガ上限を上げてもらえることが判明 。リミットを500に設定してもらい、無事に完全自動化することに成功しました。 よく調べると トラブルシューティング にも該当の内容がありました。サポートはすべて英語なので該当箇所を見つけるのも一苦労ですね……。 zapier.com 新・チェック運用 毎日のデータをAirtableに連携することで、フィルター機能で自由に表示を操作することができるようになったので、アンケートのチェック運用もAirtable上でスムーズに行えるようになりました。 まずは、曜日ごとに分担してチェックをしているため、回答を担当曜日ごとに表示できるようそれぞれのViewを作成。 曜日ごとの表示例 担当曜日の回答に目を通して 気になるものにはチェックマークをつけていき、コメント欄にメモを記入するだけでピックアップは終了 です。 MTG ではチェックがついたものについて確認して議論し、決まった対応内容を購入者様・出品者様それぞれの該当欄に記入して対応の進捗や担当者もここで管理します。 対応タスクの一覧View 更に、回答いただいたお客様へのご連絡のためのリストもAirtable上のViewで管理できるため、改めてデータ出しをする作業もなくなりました。 メール配信のためのリスト 運用変更の結果、実現したこと 今回のツールの変更と自動化で、課題だった以下の点について解決し、かなりの効率化を実現することができました。 MTG 前のピックアップのための所要時間が削減でき、 1日分のチェックに以前は1時間前後かかっていたものを30分程度でできるように なった MTG 中にチェックしたものがそのまま対応のタスクリストになるため、進捗が管理しやすくなり 対応時間の削減と対応漏れの防止 ができた 過去に出てきた類似案件の対応見直しや、特定の出品者様・購入者様に過去どういったご意見が多いかを MTG 中にさっと参照することができるようになったため、以前よりも 的確なフォローが可能に なった 回答者様への一斉連絡のためのメールリスト抽出が自動で完了できるようになり、 手動でリストを作成する手間がなくなった 今後の展望 今回の一番の目的だった業務の効率化は十分に達成することができた一方で、データ活用という観点ではまだまだやれることがあると考えています。 現在は個別のアンケートへの対応のみに活用されていますが、ピックアップしたデータを再度データベース内の他の情報と組み合わせてより高度な分析の材料としたり、特定の回答内容の方にのみMDツールと連携して自動でご案内をしたり、などデータが整ったことで活用の幅を広げることができるはず。貴重なお客様の声をサービス改善に繋げるべく、今後も試行錯誤を続けていきたいと思います。 明日の記事の担当は人事総務グループの右川さんです。お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co
はじめに こんにちは、インフラエンジニアの 高山 です。 この記事は Enigmo Advent Calendar 2021 の 9 日目の記事です。 現在、 BUYMA をオンプレから AWS へ移行するプロジェクトを進めています。 テスト環境の移行は完了し、本番環境の移行をしようというところです。 本番環境の移行をする前に 性能的に問題ないことを確認するため、本番環境と同程度のスペックで検証環境を構築し負荷テストを実施しました。 まだ終わっていませんが、今の時点で得た知見を記事にしようと思います。 負荷テストツール選定 詳細は割愛しますが、 以下のような要件から AWS の分散負荷テストのソリューション(正式名称は Distributed Load Testing on AWS 以下、 AWS 負荷テストソリューションと呼ぶ)を使うこととしました。 大規模な負荷テストができること 複雑なテストシナリオが作成できること 情報が多いこと 学習コストや構築運用コストが低いこと 費用が安価であること テストシナリオをコードで管理できること AWS 負荷テストソリューションは それ自体の情報は多くないものの JMeter の設定ファイルを読み込むことができるためテストシナリオ作成の情報は多く、 テストシナリオをコードで管理できること 以外は要件を満たしています。 ( ruby-jmeter を使えばコード管理できそうですが、手は出しませんでした。) AWS 負荷テストソリューションの概要 AWS 負荷テストソリューションは AWS のマネージドサービスを組み合わせた AWS のソリューションの1つで、 AWS が提供しているCloudFormationのテンプレートからスタックを作成すれば、簡単に作成することができます。 導入の説明などは割愛します。 以下を参考にしてください。 参考 AWS ソリューション AWS ソリューションのライブラリ AWS 分散負荷テスト ソリューション ここにCloudFormationのテンプレートが置いてあります 大規模な負荷テストを実行可能。「Distributed Load Testing on AWS」 を試してみる AWS 公式のやってみた記事 日本語のドキュメント 日本語のドキュメント 例によって、ちょっと古い 英語のドキュメント 英語のドキュメント こちらは最新 テストシナリオ作成 性能的に問題ないことを確認するためには 本番環境と同等の負荷をかける必要があります。 本番環境でのアクセスが多い機能と ログイン/未ログインの割合を調べ、それをテストシナリオにしました。 本番環境の確認 ログイン割合 未ログイン状態: 8 ログイン状態: 2 ログインページへのアクセスは300アクセスに1回程度 アクセスの多い機能の割合 Web検索: 70 Web商品詳細: 90 API 検索: 35 API 商品詳細: 35 作成したテストシナリオ 本番環境の確認結果より、1/5の確率でログインするようにしつつ アクセスが多い機能を任意の割合でアクセスするようなテストシナリオを作成しました。 作成したテストシナリオ JMeter の詳細な設定方法などは割愛しますが、ポイントは以下になります。 割合を近似してできるだけ小さい数にした インタリーブコントローラでログインの割合をコン トロール するようにした 会員IDリストの csv ファイルを用意してランダムにユーザを変えてログインするようにした (事前に同じパスワードでログインできるように仕込んでおきました) アクセスの割合で特に多い機能のHTTPリク エス ト サンプラー を割合の数だけ作成 さらにそれを5回ループし、ログインを300アクセスに1回程度になるようにした 同じ機能でも、HTTPリク エス ト サンプラー ごとに URLリストファイルを分割して別々の csv ファイルを参照するようにした (同じ csv ファイルを使うと、同一スレッドの同じ回で同じURLになってしまったため) AWS 負荷テストソリューションに JMeter の設定ファイルを読み込ませる ファイルアップロード 作成したテストシナリオで外部ファイル( csv ファイルや プラグイン ファイル)を読んでいる場合は、zipにまとめてからアップロードします。 ポイント 外部ファイルは 相対パス で指定すること テストシナリオの拡張子は jmx として、複数の jmx ファイルは含めないこと ファイルサイズ上限は50MB AWS 負荷テストソリューションのイメージと スクリプト 使用されているコンテナのイメージと実行される スクリプト を確認してみましょう。 Dockerfile を見ると、コンテナイメージはtaurusを元にしています。 ENTRYPOINTに指定されている スクリプト の中でアップロードしたファイルを読み込んでいる箇所を見てみます。 # download JMeter jmx file if [ "$TEST_TYPE" != "simple" ]; then # Copy *.jar to JMeter library path. See the Taurus JMeter path: https://gettaurus.org/docs/JMeter/ JMETER_LIB_PATH=`find ~/.bzt/jmeter-taurus -type d -name "lib"` echo "cp $PWD/*.jar $JMETER_LIB_PATH" cp $PWD/*.jar $JMETER_LIB_PATH if [ "$FILE_TYPE" != "zip" ]; then aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.jmx ./ else aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.zip ./ unzip $TEST_ID.zip # only looks for the first jmx file. JMETER_SCRIPT=`find . -name "*.jmx" | head -n 1` if [ -z "$JMETER_SCRIPT" ]; then echo "There is no JMeter script in the zip file." exit 1 fi sed -i -e "s|$TEST_ID.jmx|$JMETER_SCRIPT|g" test.json fi fi スクリプト から以下のことが わかりました。 jmx ファイルをfindで探しているようなので、zip内の jmx ファイルのパスは気にしなくても良さそうですが、含める jmx ファイルは1つだけにする必要がある プラグイン 用のjarファイルを JMETER _LIB_PATH配下へコピーしていますが、zipを解凍する前に コピーしているので プラグイン は追加できない模様 (今回のテストシナリオで使用した追加 プラグイン は Random CSV Data Set Config のみなのですが、zipに含めなくても使えました 謎です) $ docker run -it --rm --entrypoint "bash" public.ecr.aws/aws-solutions/distributed-load-testing-on-aws-load-tester:v2.0.0 root@1328dc7cdfdd:/bzt-configs# ls -l total 1296 -rwxr-xr-x 1 root root 1210 Sep 30 04:16 ecscontroller.py -rwxr-xr-x 1 root root 1360 Sep 30 04:16 ecslistener.py -rw-r--r-- 1 root root 16542 Sep 30 04:19 jetty-alpn-client-9.4.34.v20201102.jar -rw-r--r-- 1 root root 19600 Sep 30 04:19 jetty-alpn-openjdk8-client-9.4.34.v20201102.jar -rw-r--r-- 1 root root 320564 Sep 30 04:19 jetty-client-9.4.34.v20201102.jar -rw-r--r-- 1 root root 214251 Sep 30 04:19 jetty-http-9.4.34.v20201102.jar -rw-r--r-- 1 root root 164646 Sep 30 04:19 jetty-io-9.4.34.v20201102.jar -rw-r--r-- 1 root root 565135 Sep 30 04:19 jetty-util-9.4.34.v20201102.jar -rwxr-xr-x 1 root root 2998 Sep 30 04:16 load-test.sh root@1328dc7cdfdd:/bzt-configs# find ~/.bzt/jmeter-taurus -type d -name "lib" /root/.bzt/jmeter-taurus/5.2.1/lib root@1328dc7cdfdd:/bzt-configs# find /root/.bzt/jmeter-taurus/5.2.1/lib -type f -name "*jar" -ls | wc -l 109 root@1328dc7cdfdd:/bzt-configs# find /root/.bzt/jmeter-taurus/5.2.1/lib -type f -name "*jar" -ls | egrep -i "csv" root@1328dc7cdfdd:/bzt-configs# 参考 ECR Public Gallery - AWS Solutions/distributed-load-testing-on-aws-load-tester:v2.0.0 GitHub - aws-solutions/distributed-load-testing-on-aws 分析のための準備 Datadog ダッシュ ボード AWS 負荷テストソリューションのテスト結果レポートを見ても詳細な分析はできないので、詳細な分析をするためには監視ツールで必要なデータを取る必要があります。 今回はDatadogを使い、以下のようなデータを確認できるように ダッシュ ボードを作成しました。( APM も使っています) 各機能(DBやアプリサーバ、検索サーバ、キャッシュサーバ等)の負荷 アプリやLBのbusy/idle worker キャッシュサイズ、キャッシュヒット率、eviction etc 目標値 本番環境のある日の スループット /分 負荷テストした結果を分析できても、どの程度の値であればOKと判断できなければ意味がありません。 本番環境の スループット やページごとのレイテンシを調べておき、目標となる値を調査しました。 移行により性能を向上させるというよりは 現状より性能が低下せず移行できることを目標にしているため、 今回は現在の本番環境の スループット やページごとのレイテンシが そのまま目標値となります。 本番環境の全LBサーバのログを合計したところ、ピークタイムの スループット は5万アクセス程度/分でした。 ( CDN を利用しているので、オリジンのアクセスのみの計測値です) 負荷テストの流れ 以下のような流れを繰り返し、問題を解決しながら 負荷テストを進めていきました。 同時接続数を少ない数から始め、各サーバの負荷やbusy/idle worker数、 スループット を見ながら上げていく workerが枯渇する前に 各機能の負荷やレイテンシが上昇してしまう場合は、その原因を調査して解消 (設定のミスや構成的な問題、単純なスペック不足など 低レイヤーから高レイヤーまで様々な問題がでてきました) 問題が解消しworkerが枯渇した状態でも、目標となる スループット に達しない場合は アプリサーバの台数を増設 (アプリサーバの台数を増やすと、また別の場所が ボトルネック になる場合があるため、徐々に台数を増やします) 目標となる スループット に達しても、各機能の負荷やレイテンシが高くなっていなければOK AWS 負荷テストソリューションの設定値 設定項目 Concurrencyは どのように設定すれば良いのか? Task Count : <タスク数(コンテナ数)> Concurrency : <タスク毎の同時接続数(ユーザー数)> 合計の同時接続数(合計ユーザー数)は Task Count x Concurrency になります。 Task Count=1で Concurrency を増やせば安上がりなのですが、負荷をかける側にも負荷がかかるので そうはいきません。 Concurrency の推奨制限は200になっていますが、ECRの負荷を見ながら調節する必要があります。 これは ドキュメント の ユーザー数の決定 の項目が詳しかったので、ドキュメントを参照してください。 Ramp Upって必要? Ramp Up は負荷テスト開始時の暖気運転のためだと思っていて、ずっと0に設定して負荷テストしていたのですが 暖気運転以外でも Ramp Up を設定した方が良いケースに遭遇しました。 Ramp Upの設定で解消した定期的な負荷上昇 BUYMA では アプリケーションサーバ として PHP と Ruby on Rails を使用しています。 PHP で処理している機能は少ないのですが、ログイン処理は PHP を使用しています。 ログイン処理は300アクセスに1回程度ですが、 Ramp Up を設定しないと負荷テストのすべてのスレッドが同じタイミングでログイン処理をしようとするため、定期的に負荷が上がるような不可解なグラフになったと考えられます。 (時間経過とともにスレッドごとのタイミングがずれていくため、徐々に解消されていきます) Ramp Up を設定したところ、定期的な負荷上昇はなくなりました。 どれくらいの時間、負荷テストするべきか?(Hold for) 長時間負荷テスト あたりまえですが、テストをする環境や どんな負荷テストをしたいかにより、どれくらいの時間 負荷をかけるべきか変わってきます。 例えば 徐々にキャッシュがたまり、キャッシュヒット率が上がるにつれてDBへの問い合わせが減っていく様子を確認するため 長時間の負荷テストを実施しました(上記のグラフです)。 キャッシュされた状態でのテストをしたい場合はURLリストを少なくして負荷テストしました。 何をテストしたいかによって 設定を変更したり、負荷テストの時間を調節する必要があります。 AWS 負荷テストソリューションの問題 Failed to parse the results. Test Failed 長時間負荷テストを実施したり、何回もテストを作成すると AWS 負荷テストソリューションのテスト結果レポートが表示されず、 Failed to parse the results. になることがあります。 その場合は 負荷テストはできいて、レポート作成処理に失敗しているだけのようです。 ERROR finalResults function error ValidationException: Item size to update has exceeded the maximum allowed size CloudWatch Logsで確認したところ 原因はDynamoDBの制限超過エラーのようなのですが、サポートへ問い合わせたところ 仕様だそうです。 分析は主にDatadogを使用しているため、あまり支障はありませんでした。 ダッシュ ボードからテストシナリオが消えていく test-400,test-700,test-800が消えた ダッシュ ボード 何個もテストケースを作成して負荷テストしていくとなぜか、 ダッシュ ボードからテストシナリオが消えていくことがあります。 テストシナリオの数ではなく、テストの回数か何かに制限があるようです。 消えたテストシナリオでもタブが残っていれば/URLを覚えていれば、設定が残っていて、テスト実行も可能でした。 こちらはそんなに困らなかったので、サポートへ問い合わせはしていません。 サポートに項目がない サポート その1 サポート その2 明らかにDynamoDBの問題の場合はサポートに問い合わせできたのですが、 AWS 負荷テストソリューション自体の問題の場合は サポートのサービスに項目がありませんでした。 AWS 負荷テストソリューション自体の問題は SAの方に聞いてみましょう。 AWS 負荷テストソリューションの API 30分おきに Task Count を変更して負荷をかけていて API あったらいいなと思っていたのですが、 今回 ドキュメント を見返していたら、 API ありました。 見逃してました。 設定を変えてながら連続して負荷テストするような場合は API を使いましょう。 最後に 今回は大きなサービスの移行のための負荷テストで、テスト環境では発生しなかった問題が次々と発生するなど いろいろと大変でした。 負荷テストツールにはあまりコストをかけたくなかったので、 AWS 負荷テストソリューションを使うことで だいぶ ラク ができたと思います。 これから、バッチサーバなどが動いてDBに負荷をかけている状態でも 性能的に問題ないかなどを確認し、自信を持って本番環境の移行に臨めるようにしていこうと思っています。 明日の記事の担当は カスタマー マーケティング 事業本部の 杉山 さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
はじめに こんにちは、サーバーサイドエンジニアの @hokita です。 この記事は Enigmo Advent Calendar 2021 の 8 日目の記事です。 今回はテッ クリード 兼 スクラム マスターとして約8ヶ月間プロジェクトを運用していく中で学んだことを8つ紹介したいと思います。 学び 1. ストーリーポイントと難易度 例えば2ポイントのストーリーがあり、経験の長いAさんは2日、初心者のBさんは4日かかるとします。では作業量が倍と見積もった4ポイントのストーリーはどうでしょうか。Aさんは倍の4日でできたのですが、Bさんは始めての作業だったので3倍の12日かかってしまいました。このようにスキルや難易度によってポイントと 工数 が単純な比例関係にならないことがよくあります。そのため、メンバーのスキルを認知しつつ、どのような策をとるべきか考える必要がありました。 納期が迫っているなら、Bさんが苦手なタスクをAさんにやってもらう スケジュールに余裕があるなら、Bさんに苦手なタスクをやってもらいながら スキルアップ を目指す Aさんに余力があるなら、Bさんを手伝ってもらう( ペアプロ を実施するなど) それぞれメリット・デメリットあるので、その時々で判断する必要があるかと思います。 2. フル稼働ではなく1名助人役になる プロジェクト開始時は私を含めた2名で開発していました。そのときのベロシティは約7ポイントで、途中で人員を増やし4名になってからは約14ポイント消化できるようになりました。人数が倍になったからベロシティも倍になったと思うかもしれませんが、そうではなく、私はあまり開発をせずに助人役に回っていました。実際には下記のようなことを行っていました。 進捗が著しいタスクを発見して対策を考える ストーリー着手前に一緒に設計を考える コードレビュー 手が空いたときには小さめのストーリーを消化 私が開発に集中することもあったのですが、進捗は逆に低下することが多かったです。開発中に発生する問題は思った以上に 工数 を膨らませます。それを解消する役がいることで安定した開発スピードを出すことができると気づきました。 3. レビューファースト スクラム 開発ではスプリント内で成果物を残し、 ステークホルダー へデモを見せフィードバックを貰うことが重要です。よくあったのが、一人で同時に複数ストーリーを進めて、結局どのストーリーもスプリント内に終わらせることができなかった、というものです。なぜそのようなことが発生するかというと、レビューを返すまでに時間がかかっていることが原因でした。レビュー依頼を出した開発者はレビューが返ってくるまでは他のストーリーに着手するかと思います。そうしているうちに複数ストーリーの マルチタスク となって、結局どのストーリーも消化できずじまいとなってしまいます。レビューはなるべく早く返して、1つのストーリーを確実に終わらせることが大事です。 4. ベロシティがプレッシャーに 良くなかったなと反省しているのですが、1on1の時に各開発メンバーに「次のスプリントでは○ポイントの消化を目指しましょう」とストーリーポイント基準で目標を設置していました。数値目標で管理しやすいと思っていたのですが結果どうなったかと言うと、コードの品質が下がり、時には仕様を満たないプルリクがくることもありました。個人のコーディングスピードはいきなり上がるものではないので、時間を省くとなれば デバッグ 時間となっていたのだと思います。それに気づいた後は、まずは安定したアウトプットができること、そして、ポイントはあくまで目安に過ぎないことを意識し、目標は消化ポイントとは別のものに変更しました。 5. レトロスペクティブが自己評価になりがち レトロスペクティブではメンバーそれぞれが KPT 法で書いていました。そこでよく上がってくるものは「〇〇の実装で時間がかかってしまった。なので、〇〇を勉強する」のような自己評価が多かったです。個人の能力を伸ばすことも重要ですが、どちらかというとチームとしてなにができるのかを議論することのほうが重要だと思っています。「〇〇で時間がかかった。」のは相談する機会がなかったのが原因なら「朝会で相談する(相談しやすくする)」やスキルが足りない場合は「詳しい人と ペアプロ の時間を設ける」というのが良い振り返りかと思います。これは開発している本人だと気づけないことが多いので、他の人が提案してあげることが望ましいです。 6. スプリント内で終わらないストーリーは放置しない 前述したとおりスプリント内で成果物を残すことは重要なことですが、どうしてもストーリーを消化できないことは多々発生します。ストーリーの粒度を小さくすることは心がけていたもののどうしてもそうはならないストーリーもありました。ほとんどの場合次の週にも継続して開発するのですが、なぜ終わらなかったのかを調査し対策することが大切です。例えば一人で行き詰まって終わらなかったのなら、次週は ペアプロ でそのタスクを最優先で終わらせる、もし思っていた以上に作ることがあった(例えばAという機能を追加するのに実はBという機能を作る必要があったなど)なら、まずストーリーを分解することはできないか、他のメンバーと役割分担はできないか、今のまま続けるとしたらどのくらいかかりそうか、など念入りに調査し対策を考えます。これを怠ると何スプリントにもまたがるストーリーになる可能性があり、開発者のメンタルを下げ、負のスパイラルに陥ることが多い印象です。 7. 早くリリースしたいなら機能を削る まず下記を スクラム チーム全員で認識を合わせる必要があるかと思います。 開発スピードが劇的に向上しないこと 最初に作成した仕様の大半は不要な機能であること それを前提に納期へ向けてできることといえば「機能を減らす」もしくは「納期を伸ばす」しか手段はないかと思います。今回のプロジェクトも バックログ を全て消化するにはベロシティなどの数値から計算して目標納期に間に合わせるのは「不可能」でした。やったことと言えば、優先度の低い機能をごっそりと削ることでした。リリース後にその削った機能を開発したかというとほとんどの機能は「不必要」でした。 8. 興味を引くスプリントレビューを 本プロジェクトはバックエンドのメンバーが多かったので、デザインは後回しにして簡素なページを作成していき、デモでは毎週動くものを提供していたのですが、 ステークホルダー からのフィードバックが薄いことに気付きました。その後デザインが当たった段階でやっといろいろな意見をいただけるようになりました。動けば良いというものではなく、もしデザインファーストなプロトタイプを作っていればもっと早い段階で多くのフィードバックを貰えたかと思われます。機能や ステークホルダー のよりけりだと思いますが、興味を引くようなプロトタイプを作ることも重要だと気づきました。 最後に いかがだったでしょうか。まだまだ未熟ですが、今回の学びを次回のプロジェクトへ生かしていこうと思います。 明日の記事の担当は インフラエンジニア の 高山 さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
こんにちは、 エニグモ でデータアナリストをしている井原です。 この記事は Enigmo Advent Calendar 2021 の 7日目の記事です。 今日は、実際に業務で、データ分析をした内容を元に、データアナリストがどのような仕事をしているのかをお話したいと思います。 データアナリストの仕事 世の中では、データアナリストと言われる職種の仕事は多岐に渡ると思います。 データマイニング 、データ分析基盤の整備、ビジュアライゼーション、KPIの設計、 機械学習 モデルの構築、etc... エンジニアリングやサイエンスの領域と思われるところを担っているデータアナリストの方もいらっしゃるのではないかと思います。 エニグモ の場合、データサイエンティストやデータ基盤エンジニアといった、専門家が在籍しています。そのため、データアナリストは、施策の効果検証やサイト上の課題発見といった、ビジネス領域の課題に対して、データ分析で解を出す仕事にフォーカスすることが多いです。 また、 エニグモ は、データ分析の リテラシー が高く、データアナリストではないディレクターといった職種の人でも、 SQL を回して、データの抽出/分析を行うことが普通の文化になっています。 データアナリストとしては、データ分析の設計や手法を深く理解して、アウトプットを出していくことのやりがいを感じながら、仕事の出来る環境になっていると思います。 決定木による売れ筋商品の分析 ここからは、実際に分析した例を元にして、分析手法として使用した決定木分析について、お話したいと思います。 課題 エニグモ が運営している BUYMA では、CtoCの売買を仲介するプラットフォームビジネスを行っています。 そのため、 ECサイト として、購入者だけではなく、販売を行う出品者に対してのフォローも行うことが必要です。出品者の方に良質な商品を出品していただくことで、売り場としての魅力が向上し、購入者にとっても、良質なサイトになっていくと考えられます。 しかし、良質な商品とは何なのか?想像するものは、人それぞれで異なると思います。そこを、定性的な感覚だけでなく、 定量 的なデータ分析を行うことで、売り場にあるべき商品を定義し、出品者の方に出品促進を行っていきたい、というのが、今回の課題でした。 分析方法の選定 良質さを決める要素は数多くあると思いますが、今回はまず、基礎的な分析として、商品のブランド、カテゴリ、モデル名の中で、どのような商品が売れているのかを調査しました。また、売れている商品、の定義については、ビジネス側のメンバーと議論のうえ、CVR(出品された商品数のうち、販売された商品数の割合)としました。 早速、日別のデータを取得し、 BUYMA の中で主流となるジャケットカテゴリに絞ると、以下のようなデータが確認できます。 ※なお、記事内で取り扱っているデータについては、全て、ダミーデータとなります。 BUYMA で取り扱われているブランド、カテゴリ、モデル名は、数が多く、クロス集計などで解釈することは困難と思われます。 ブランド、カテゴリ、モデル名をそれぞれ単体で集計することも可能ですが、その場合、あるブランドのCVRが高いと、どのようなカテゴリ、モデルでも高いのか?といった解釈が難しくなります。 今回は、CVRに対して、ブランド、カテゴリ、モデル名といった特徴のうち、どの要素の影響が大きいのか?を分析したいですので、可視性が高く、解釈性のよい決定木分析を使って、分析してみることにしました。影響の大きさを見るには、重回帰分析といった手法もありますが、決定木分析であれば、要素の掛け算(このブランドのこのモデルのCVRが高い、といった見方)も確認できます。 ※厳密には、重回帰分析でも要素の掛け算を変数とすることで、出来ないことはありませんが。 実装 python を使って、実装していきます。 先ほど取得してきたデータのうち、ブランド、カテゴリ、モデル名、を説明変数とし、CVRを目的変数として予測する決定木モデルを作成します。 環境: Windows 10 Pro Python 3.9.9 1.ライブラリのimport 必要なライブラリをインポートします。 import pandas as pd import subprocess # 可視化を行うためのライブラリ import matplotlib.pyplot as plt # 回帰の決定木モデルを作成するためのライブラリ from sklearn.tree import DecisionTreeRegressor, export_graphviz from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score from sklearn.metrics import mean_absolute_error 2.データの読み込み pandasでデータを読み込みます。 df = pd.read_excel( "cvr_data.xlsx" )[[ "date" , "brand" , "cate_name" , "model" , "listing_count" , "sell_count" ]] # 必要な列に絞る # データの確認 print (df.head()) print (df.columns) 3. 移動平均 に変換する ECデータの場合、平日より休日の方が多く売れる傾向がありますので、7日間 移動平均 に変換して、データを均します。 ローデータは、前の要素の9/30の次に次の要素の9/1が来てしまうため、9/7以降のデータに絞り込みます。 ※もっとよいやり方がありそうな気がしますが、自分の知識だとこうなりました。 df[ "listing_count" ] = df[ "listing_count" ].rolling( 7 ).mean() df[ "sell_count" ] = df[ "sell_count" ].rolling( 7 ).mean() df = df[df[ "date" ] >= "2021-09-07" ] # データの確認 print (df.head( 30 )) # CVRを計算して、カラムを追加 df[ "cvr" ] = df[ "sell_count" ]/df[ "listing_count" ] 4.変数をダミー変数に変換 今回、使用する予測変数は、全て質的データになるので、そのまま、決定木分析に使用することは、出来ません。 get_dummies関数を使って、ダミー変数に変換します。 df = pd.get_dummies(df, drop_first= True ) # 2の時点と異なることを確認 print (df.columns) 5.データの分割 予測変数と目的変数、学習用データとテスト用データに分割します。 今回は、モデルの精度を上げることは目的としていないため、テストデータは少なくして、ほとんどのデータを学習データにしました。 exclusion_list = [ "cvr" , "date" , "listing_count" , "sell_count" ] include_list = [column for column in df.columns if column not in exclusion_list] obj_df = df[ "cvr" ] exp_df = df[include_list] obj_array = obj_df.values exp_array = exp_df.values X_train, X_test, Y_train, Y_test = train_test_split(exp_array, obj_array, test_size= 0.01 , random_state= 222 ) 6. 決定木モデルの学習 作成したデータで、決定木モデルを学習させます。 # モデルのインスタンス生成 reg = DecisionTreeRegressor(max_leaf_nodes= 20 ) # 学習によりモデル生成 model = reg.fit(X_train, Y_train) print (model) # 評価 y_true = Y_test y_pred = model.predict(X_test) print (r2_score(y_true, y_pred)) print (mean_absolute_error(y_true, y_pred)) 7. 木構造 を画像に保存 モデルの 木構造 を解釈できるよう、画像に変換します。 dot_data = export_graphviz(model, out_file= "cvr_data.1.dot" , filled= True , rounded= True , feature_names=exp_df.columns ) subprocess.run( "dot -Kdot -Tjpg -Nfontname='MS Gothic' -Efontname='MS Gothic' -Gfontname='MS Gothic' cvr_data.1.dot -o cvr_data.jpg" .split()) # 日本語を含むと、文字化けするため、fontを指定 解釈 以上の ソースコード を実行すると、以下のような決定木のjpgファイルが出来上がります。 出来上がった決定木を見ながら、解釈をしていきます。 注意点として、決定木は、lossが少なくなるように分割していく アルゴリズム であるため、上位に出てくる変数が、必ずしも、CVRを高くする変数とは限りません。 value を確認しながら、どのような分割がなされているか、確認していきます。 まず、カテゴリ_jacketGが最初のノードで分割されるようになっています。 そして、右に分割されたノードの value は0.108と左の0.024のノードよりも高いため、カテゴリが、カテゴリ_jacketGの場合、CVRがかなり高くなると解釈できます。 では、カテゴリ_jacketGであれば、なんでもよいかというと、その次の分割を見てみると、ブランドがブランド_BNである場合、 value が0.142、そうでない場合は、0.006となっているため、カテゴリ_jacketGは、ブランド_BNが一強のカテゴリであることが分かります。さらにノードを下ると、ブランド_BNの中でもモデルによって、CVRは異なるようですが、全般的には、高いCVRを擁していることが見てとれます。 カテゴリ_jacketGではないノードを見ていくと、いくつかのブランド名でノードが枝分かれするようになっています。カテゴリ_jacketGでなければ、その次は、ブランドの選択が重要である、ということが見てとれます。実際には、企画担当者と会話をしながら、表示されているブランドをグルーピングなどして、整理しました。 さらに深く確認しようと思えば、ブランド_BFは、カテゴリによって差がある、カテゴリ_jacketBカテゴリかどうかで、ノードが分かれる、と状況に応じて、確認していくことも可能です。 決定木の場合、初めにも話した通り、視覚的に分析結果を表せるため、 ドメイン 知識が少なくても、結果の解釈が行いやすいことはメリットではないかと思います。また、企画担当者側も分析結果が分かりやすいので、スムーズに相談が行いやすくなると思います。 なお、解釈性が高い決定木分析ですが、注意点もあります。 まず、決定木分析は 機械学習 の アルゴリズム の中では、精度が高くなりにくい、と言われています。これは、モデルが学習データに 過学習 しやすく、汎用性が低くなってしまうためです(今回は、生データや、感覚値ともずれていないという判断をして、精度はあまり重視しませんでしたが。)。決定木に限りませんが、あくまでも学習データとして使用したものの説明にしかなっていませんので、将来的にも同じ傾向があるかどうかは、確実ではありません。特に、一時的に強い需要があったデータなどが含まれると、当然、そのデータの影響が強く出てしまうため、注意が必要です。 今後の展望 今回は、比較的、カジュアルな分析でしたので、そこまで、多くない変数で実施しました。感覚としては、企画担当者側も理解しやすかったのではないかと感じましたので、決定木を使ったデータ分析は有用であると考えています。 変数を増やしていくことで、目的とする変数に対して、どういった変数が影響を与えているのか、さらに詳細な分析を行うことも可能と考えられます。 また、決定木 アルゴリズム の発展形として、LightGBMやXGBoostなどの アルゴリズム が、データサイエンス分野では、スタンダードになっているようです。他にも、SHAPなど、今回、実施した内容以外で、 機械学習 モデルの可視化をする方法が研究されており、自分も現在、勉強中です。 最初にお伝えした通り、 エニグモ には、データアナリストとは別に、データサイエンティストの職種もあります。データサイエンスのプロフェッショナルがいて、通常のデータ分析を企画担当者の方も普通に行っている環境ですので、ビジュアライゼーションやモデルの説明性といった手法を使って、データとビジネスをうまくつなげていくのが、データアナリストの役割ではないかと考えています。 本日の記事は、以上です。読んでいただき、ありがとうございました。 明日の記事の担当はエンジニアの沖田さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
この記事は Enigmo Advent Calendar 2021 の6日目の記事です。 こんにちは。 BUYMA でWebエンジニアをやっております、岡本です。 エニグモ に入ってから約1年が経過しました。 学生の時にプログラミングを始めてしばらくbashを使っていたのですが、イケイケの先輩に zsh を教えてもらい、 zsh の機能を拡張するための プラグイン マネージャーにはOhMyZshを勧められ、数年利用していました。この時期に使っていた macOS の標準シェルは bash で、 zsh はわざわざインストールするものでした。 1年前に エニグモ に入社した頃、支給されたMacBookProに搭載されている macOS の標準シェルは既に zsh になっていました。(標準シェルが zsh になったのは macOS 10.15 Catalina以降です) せっかくの機会なので気持ちを新たに プラグイン マネージャーも替えてみようと思い立ち、いくつか zsh の プラグイン マネージャーを調べたところZinitが良さそうに思えたので導入することにしました。今回は導入した感想を軽く綴ってみようと思います。 github.com (2021年12月6日閲覧、以下記載URLも同様) いいところ いくつか公式ドキュメントでもアピールされていますが、個人的に良いと思うところを挙げます。 zsh の起動が速くなる 公式ドキュメントで喧伝されているのが zsh の起動スピードについてです。早いことをアピールしています。 Zinit is currently the only plugin manager out there that provides Turbo mode which yields 50-80% faster Zsh startup 訳(Zinitは現在、ターボモードを提供する唯一の プラグイン マネージャーであり、 Zsh の起動が50〜80%速くなります。) cf. https://github.com/zdharma-continuum/zinit#zinit ここでターボモードとは何ぞやとなるのですが、 wait を使った遅延読み込みのことを指すようです。これについては後ほど紹介しますが、ターボモードを使わなくても従来の プラグイン マネージャーと比較すると zsh の読み込み速度は高速になっていると思います。 OhMyZshおよびPrezto プラグイン とライブラリの読み込みをサポートしている OhMyZshやPreztoなどを利用していた方もその資産を継承できます。なお私はOhMyZshの プラグイン は現在は読み込まずに使っています。気になる方は以下をご覧ください。 参考URL https://zdharma-continuum.github.io/zinit/wiki/INTRODUCTION/#oh_my_zsh_prezto 使ってみよう Zinitのインストール方法はこちらからご覧いただけます。 https://github.com/zdharma-continuum/zinit#automatic-installation-recommended sh -c " $( curl -fsSL https://git.io/zinit-install ) " source ~/.zshrc zinit self-update こちらを実行することで ~/.local/share/zinit/zinit.git にzinitがインストールされ、zshrcにzinitの設定が追加されます。 ここから自分好みにカスタマイズしていくわけですが、現時点の私のzshrcを見てみるとこんな感じになってました。1~7行目は romkatv/powerlevel10k を使うために設定しているものです。一言でいうとターミナルのUIをカラフルにしてくれるものです。 ペアプログラミング の機会があると「ターミナルのUIがカラフルですね」とよく言われます。 ディレクト リ名やブランチ名が見やすいのでOhMyZshを使っている時からずっと使っています。 1 # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. 2 # Initialization code that may require console input (password prompts, [y/n] 3 # confirmations, etc.) must go above this block; everything else may go below. 4 if [[ -r " ${XDG_CACHE_HOME:-$HOME/.cache} /p10k-instant-prompt- ${(%):-%n} .zsh" ]]; then 5 source " ${XDG_CACHE_HOME:-$HOME/.cache} /p10k-instant-prompt- ${(%):-%n} .zsh" 6 fi 7 8 ### Added by Zinit's installer 9 if [[ ! -f $HOME /.zinit/bin/zinit.zsh ]]; then 10 print -P "%F{33}▓▒░ %F{220}Installing %F{33}DHARMA%F{220} Initiative Plugin Manager (%F{33}zdharma/zinit%F{220})…%f" 11 command mkdir -p " $HOME /.zinit" && command chmod g-rwX " $HOME /.zinit" 12 command git clone https://github.com/zdharma/zinit " $HOME /.zinit/bin" && \ 13 print -P "%F{33}▓▒░ %F{34}Installation successful.%f%b" || \ 14 print -P "%F{160}▓▒░ The clone has failed.%f%b" 15 fi 16 17 source " $HOME /.zinit/bin/zinit.zsh" 18 autoload -Uz _zinit 19 (( ${+_comps} )) && _comps[zinit]=_zinit 20 21 # Load a few important annexes, without Turbo 22 # (this is currently required for annexes) 23 zinit light-mode for \ 24 zinit-zsh/z-a-rust \ 25 zinit-zsh/z-a-as-monitor \ 26 zinit-zsh/z-a-patch-dl \ 27 zinit-zsh/z-a-bin-gem-node 28 29 ### End of Zinit's installer chunk 30 以下略 さて、 プラグイン を設定してみます。 zsh を使う上で外せないのは補完と シンタックス ハイライトではないかと思います。 # 補完 zinit light zsh-users/zsh-autosuggestions # シンタックスハイライト zinit load zdharma/fast-syntax-highlighting ここで、 プラグイン の読み込み方法は load と light があります。 load を使うことで プラグイン のト ラッキング を可能にします。 zinit report {plugin-spec} で プラグイン の情報を出力し、 zinit unload {plugin-spec} で プラグイン を無効にします。 一方 light を使うと プラグイン のト ラッキング 機能が無効になり、loadに比べて読み込み速度が速くなるようです。 また snipet を使うと、URLを直接指定する形で プラグイン を読み込むことができます。 zinit snippet https://gist.githubusercontent.com/hightemp/ 5071909 /raw/ cf. https://zdharma-continuum.github.io/zinit/wiki/INTRODUCTION/#basic_plugin_loading このような感じでお好みの プラグイン を追加していきましょう。筆者のzshrcをみると zdharma/history-search-multi-word というコマンド履歴を検索する プラグイン が入っています。 zinit load zdharma/history-search-multi-word 設定例は zinitのドキュメントに記載されている ので私はこれを参考にして設定したのですが、ここで ice というものが出てきます。 zinit ice pick "async.zsh" src "pure.zsh" zinit light sindresorhus/pure zinit ice depth= 1 ; zinit light romkatv/powerlevel10k zinit ice は直後の行で実行される load や light の挙動を変更します。 ice の後の pick や src などはice-modifiersと呼ばれるもので、 iceという名前の由来は、氷は飲み物に入れて少し経つと溶けることから、変更が一時的なものであることを意味すると公式ドキュメントでは説明されています。ice-modifiersの後ろにクォーテーションやイコールで指定されているのは引数です。ice-modifiersが引数によってzinitの挙動を制御します。例えば上記の pick であれば、引数として与えられた"async. zsh "に実行権限を与えてPATHに追加するようにzinitに対して指示しています。 その他のice-modifiersの用法についてはこちらを参照してください。 https://github.com/zdharma-continuum/zinit#ice-modifiers おわりに 基本的な使い方はできていると思いますが、まだまだ知らないオプションが多いのでもっと使いこなせるように日々精進したいと思います。 次回の記事の担当はデータアナリストの井原さんです。お楽しみに!! 株式会社 エニグモ 正社員の求人一覧 hrmos.co