元SEがコーポレートエンジニアに転職してみた

こんにちは! 今年7月に中途入社しました、コーポレートエンジニア(コーポレートIT[CO-IT]チーム) のフルセです! 今年も終盤(早いですねぇ、、)ということで、 Enigmo Advent Calendar 2023 の季節になりました!! クリスマスイブである24 日目を担当する私は入社エントリ・振り返りなど中心に自由に書きたいと思います!

なお、この記事が少しでもコーポレートエンジニアに興味がある方や入社を検討してくださっている方の参考になれば幸いです。

Embed from Getty Images

簡単な経歴紹介

私の経歴についてざっくり下記のようにまとめました。

① 新卒でシステムインテグレータ企業にSEとして入社

→ITの基礎知識・技術を獲得! and IT業界の厳しさを痛感、、(大袈裟)

② システムインテグレータ企業を退職

→主にシステム相手の業務だったので、より人と接する仕事がしたいと考え決意

③ 某アパレル商社の情シスへ転職

→培った知識とスキルを活かし新たな業種へチャレンジしたかった!

→情シスが意外と性に合っていることに気づくきっかけに!

④ カナダへ留学

→ずっと挑戦したかった海外留学に挑戦!

→語学はもちろん人との出会いや経験から人生にプラスになった!

エニグモにコーポレートエンジニアとして入社

→帰国後にご縁があり、経験を活かしコーポレートエンジニアとして入社!

そもそもコーポレートエンジニアって何?

コーポレートエンジニアとは?
コーポレートエンジニアとは、企業内のIT活用や運用を担当するエンジニアのことです。

企業のIT環境の変化は著しく、テレワークの普及などによってクラウドサービスの利用も活発になり、SaaS型のID管理や統合認証サービスを利用する企業が増えてきました。そのなかでコーポレートエンジニアは、社内ITの構築・運用をはじめとして、社内業務の課題解決のための企画立案や部門間の調整まで幅広い業務を担います。

もっと詳しく知りたい方は、チームメンバーの記事を読んでみてください! tech.enigmo.co.jp

実際に入社してみて感じたこと

コーポレートエンジニアとして働く以上、社員の方々とのコミュニケーションをかなり重要だと考えていました*1。 しかし、エニグモはリモートワーク中心の会社のため、Face to Face でのお話しする機会が少なくどのように交流の輪を広げていこうか少し不安(そもそも入社直後というのもあり。。)に感じていました。

そんなときに、2M(Monthly Meet-up)*2に参加させていただきました。 そこでは色々な部署や役職の方と分け隔てなくフランクにお話しすることができ、一瞬にして不安が和らぎました。 やはり、新しい環境でのスタートはどうしても孤独感や不安がつきものだと思いますが、こういった交流会があると気持ちよくスタートダッシュが切れますよね!そもそも、こういった交流会を定期的に行なっている企業というのは少ないと思うので、これぞエニグモの良いところだと思っております。

それからというもの、毎月可能なかぎり参加し交流の輪を広げております!(無料で美味しいお酒とご飯が得られることも理由の一つなのですが笑)

2Mについて気になる方は、レポート記事がありますので是非ご覧いただければと! www.wantedly.com

入社してからの仕事とその感想

入社してからのざっくりとした仕事内容とその感想を書かせていただきました!

内容

  • 社内ヘルプデスク対応

    • 内容:PCの故障や不具合、各種ITサービスのトラブル対応等
    • 対応頻度:1〜2件/日
  • アカウント管理

    • 内容:アカウントの付与・削除から数量管理・購入
    • 対応頻度:1〜2件/週
  • IT機器の管理・キッティング

    • 内容:PCやモバイルデバイスの発注・管理・キッティング
    • 対応頻度:10〜20件/月
  • 社内ナレッジの作成・整備

    • 内容:手順書やルールの新規作成・ブラッシュアップ
    • 対応頻度:2回/週
  • 中途入社者向けのオリエン

    • 内容:PCのセットアップ方法の説明、社内ITサービスルールの共有
    • 対応頻度:1〜2回/月
  • 社内不要OA・IT機器廃棄対応

    • 内容:廃棄物回収対応
    • 対応頻度:2回/年
  • カオスマップ作成

    • 内容:社内で利用中サービスを整理と今後導入が必要なサービスの洗い出し
    • 対応頻度(見直し):2回/年

感想

一覧にしてみると、今年は色々なことをやらせていただいたと改めて感じています。 裁量を任せていただいているため、日々にマルチタスクをこなしておりますが、 それもまた自分のタスク管理力や遂行力の向上につながっていると感じます。

個人的にやりがいがあった業務としては、「社内不要OA・IT機器廃棄対応」です。 業者選定から始まり、やりとり(契約締結や回収日の調整など)、社内ナレッジ作成、各方面との連携など、これぞコーポレートエンジニアと感じるようなタスクだったため、とてもやりがいがありました。 それと山積みだった廃棄端末類をスッキリさせることができたという達成感が大きかったですね笑

またタスクとして面白かったものとしては、「カオスマップ作成」です。 初めてカオスマップというものを作成したので、そもそもカオスマップとは?からスタートし、他企業のカオスマップの調査、社内ITサービスの整理、今後導入が必要なITサービスの洗い出しを行いました。その後は、カオスマップをパズルのように作成する大変な作業がスタートし...と、ここでは書ききれないので、詳しい作成プロセスなどは機会があればまた書かせていただきたいと思います。(今回は割愛させていただきます。) それからというもの、試行錯誤を繰り返し、レビューを重ねてやっとの思いで完成させることができました。 結果として、時間をかけて取り組んだからこそ、サービスに対する理解や知見を広くできましたし、社内の課題(足りていないサービス)も炙り出すことができました。とはいえ、まだまだ未熟なカオスマップだと思っているので、これからもっと成熟させていきたいと思います。(楽しみ!)

こんな方に向いているかも(個人見解)

コーポレートエンジニアとしてのキャリアをスタートしたばかりの私ですが、 そんな私だからこそ考えるコーポレートエンジニアにはこんな人が向いているのではないかというのをまとめてみました!

私が思うに下記の3つの要素が肝になってくると思います。

コーポレートエンジニアの必要要素

  • 新しい情報・技術に興味があり探求するのが好きな方(好奇心)

    • 日々進化するIT技術やサービスをいち早く自社へ展開・導入するため
    • 最新の情報に敏感であれば、サービスなどがアップデートされたとしても柔軟に対応することができるため
  • 人とコミュニケーションするのが得意・好きな方(コミュニケーション力)

    • 業務上(問い合わせ対応・情報共有など)何かとコミュニケーションが必要になるため
  • 問題を切り分けて解決することが得意・好きな方(柔軟性)

    • コーポレートエンジニアは企業の多くの課題を与えられるので、一つ一つ解決し柔軟に対応することが求められるため

ここで書かせていただいた3つの要素は、かなり重要なポイントになると考えます。 コーポレートエンジニアの業務は、IT関連の何でも屋のようになりがちだと思っております。 そのためタスクを挙げ出したらキリがないうえ、日々変化します。 そのため、柔軟性はもちろん、社内外連携のためのコミュニケーション力やITサービスに関する知見を広げるための好奇心が重要になってきます。 もちろん、足りてないと感じている要素があったとしても、業務を通じてレベルアップすることは可能だと思います!(私も絶賛レベルアップ中...)

※あくまで個人の見解なのでご了承ください。

最後に

私もまだまだ未熟なので、日々進化するIT技術・情報に置いていかれないよう自己研鑽を続けつつ、 社内の課題を一つずつ着実に解決し、より良い社内環境づくりに励みたいと思います!

最後まで読んでいただき、ありがとうございました!!


株式会社エニグモ すべての求人一覧

hrmos.co

*1:情シス時代に業務上のコミュニケーションがかなり重要だと理解していたため

*2:毎月月初に開催される社内交流会

Argo Workflowsを使ったKubernetes(EKS)のアップグレード

こんにちは、インフラグループ Kubernetesチームの福田です。
この記事は Enigmo Advent Calendar 2023 の22日目の記事です。

皆さんはKubernetesのアップグレード、どうしていますか?
Kubernetesは4ヶ月に一回、新しいマイナーバージョンがリリースされ、最新の3つのマイナーバージョンのみサポートされます。
つまり、原則は4ヶ月に一度、アップグレードをやらなければなりません。(最新バージョンであれば最大12ヶ月はサボれるという考え方もありますが。。。)
弊社ではKubernetes環境としてEKSを使っており、Kubernetes本体のリリースと微妙に間隔が違いますが、最新を維持するために大体3〜5ヶ月毎にアップグレード作業をやっています。
このKubernetes(EKS)のアップグレード作業をワークフロー化したのでそれを紹介します。

ワークフローとは

本記事でいうワークフローとはワークフローエンジンを使って自動化された作業のまとまりです。
ワークフローエンジンとは、作業プロセスをいい感じに管理してくれるツールです。
代表的なものとして、Apache Airflow, digdag, Argo Workflowsなどがあります。
我々の場合、Argo Workflowsを利用しています。

EKSアップグレードワークフローの概要

上図は実際のワークフローのキャプチャです。
3段階の構造(上から順に実施される)になっていて、以下を実施する内容となっています。

  1. 廃止されるAPIを使っていないかチェック
  2. EKSアドオンのアップグレードバージョンを特定
  3. CloudFormationテンプレートを更新するPullRequestを出す。

元々、弊社ではAWSリソースをCloudFormationで管理する運用になっていたことから、ワークフローのゴールがCloudFormationコードの修正(Pull Request作成)となっています。

ワークフローの入力

上図の通り、ワークフローには入力パラメータがあります。

  • ticket-id
    • JIRAのチケットIDを指定します。 コード修正時のコミットメッセージやブランチ名のプレフィックスとして利用されます。
  • kubernetes-cluster
    • アップグレードを実施する対象のクラスタをプルダウン形式で選択します。
  • eks-version
    • Kubernetes versionをマイナーバージョンまで指定します。例:1.28
  • ami
    • EKSノードで使用するAMIを指定します。

EKSアップグレードワークフローの詳細

1. 廃止されるAPIが無いかチェック

Kubernetesではマイナーバージョンの更新に伴って、機能追加や古い機能の削除のためにAPI Versionが更新され、既存のものが廃止されることがあります。
そのため、Kubernetesアップグレードを実施する前に廃止されるAPIが無いかチェックしています。
plutoというOSSを使って、クラスタ上にあるリソースで廃止されるものを使っていないかチェックをしています。

2. EKSアドオンのアップグレードバージョンを特定

EKSではKubernetesのマイナーバージョンの更新と併せて、EKSアドオンのアップグレードが必要な場合があります。我々の場合、「EKSアドオンのバージョンはデフォルトバージョン※を指定する」というポリシーで運用しています。
※ EKSではアドオン毎にKubernetesのマイナーバージョンに紐づくデフォルトのバージョンを公開しています。

具体的な方法としてはawsコマンドで、指定したKubernetesのマイナーバージョンに対応するEKSアドオンのデフォルトバージョンを取得しています。

aws eks describe-addon-versions --addon-name ADDON_NAME --kubernetes-version K8S_NAME --query 'addons[0].addonVersions[?compatibilities[0].defaultVersion==`true`].addonVersion | [0]'

3. CloudFormationのコードを管理しているリポジトリKubernetesアップグレードのためのPull Requestを出す

このステップについては前のステップで得られるEKSアドオンのアップグレードバージョンとワークフローへの入力値を元に、CloudFormationのコードを修正して、Pull Requestを出します。
また、Pull Requestに人手で実施する作業内容を記載します。
このステップにおける処理はGoで実装した自前のCLIツールで行っています。

考慮事項

このステップを実装するにあたり、考慮したことを紹介します。

VPC CNIのアップグレード

前のステップでEKSアドオンのアップグレードバージョンを特定していますが、VPC CNIについてここで問題があります。
VPC CNIのアップグレードは一度に1つのマイナーバージョンのみアップグレード可能です。
例えば、Kubernetes 1.27に対応するVPC CNIのデフォルトバージョンがv1.12.6-eksbuild.2Kubernetes 1.28に対応するVPC CNIのデフォルトバージョンがv1.14.1-eksbuild.1の場合、予めVPC CNIのバージョンをv1.13系にアップグレードする必要があります。
実際の実装としてはVPC CNIのアップグレードバージョンのマイナーバージョンが元のバージョンより2つ以上離れている場合は、本ワークフローを実施する前にVPC CNIのマイナーバージョンをアップグレードするように促すメッセージを出してワークフローはそこで失敗するようにしています。

手順書

Kubernetesアップグレードの実施自体の手順としてはCloudFormationの実行コマンドだけですが、実際の作業手順はもう少し複雑です。
我々の場合はアップグレードの実施コマンドに加えて以下を盛り込む必要がありました。

  • 作業前
    • 作業通知の開始アナウンス
    • 作業時の監視アラートのサイレント設定
  • 作業後
    • アップグレードが正しく完了したかの確認
    • 作業時の監視アラートのサイレント設定解除
    • 作業通知の終了アナウンス

yamlをGoで編集する際の副作用

CloudFormationのテンプレートはyamlですが、これをGoで書き換えると発生する副作用があります。
それはGoでyamlを扱うためにyamlファイルをエンコードして、プログラム上で何らかの処理をして、再度yamlファイルに戻してやると空行やコメント等が消失してしまうというものです。
yamlとしては等価なのだからという理由で切り捨ててしまえばそれまでですが、コメントや空行はリーダビリティの観点で有用なものも多いので、これは維持するようにしたいです。

まず、コメントの維持についてはyaml.v3を使うことで解決できます。
具体的には以下のようにyaml中にある書き換えたいプロパティをpathで指定して、それをnewValueで更新するようにしています。

func (e *yamlEditor) updateYamlProperty(node *yaml.Node, path []string, newValue string) error {
    if len(path) == 0 {
        return fmt.Errorf("invalid path")
    }
    for i := 0; i < len(node.Content); i += 2 {
        keyNode := node.Content[i]
        valueNode := node.Content[i+1]
        if keyNode.Value == path[0] {
            if len(path) == 1 {
                valueNode.Value = newValue
                return nil
            } else if valueNode.Kind == yaml.MappingNode {
                return e.updateYamlProperty(valueNode, path[1:], newValue)
            }
        }
    }
    return fmt.Errorf("invalid path")
}

空行についてはdiffコマンドのオプションで空行を無視する差分のdiffを生成して、それをpatchコマンドで元のファイルに修正として適用するようにしています。

## 元々存在するcloudformation template
target.yaml

## プログラムによって書き換えが行われたcloudformation template。空行はなくなってしまっている。
target.changed.yaml

diff -U0 -w -b -B target.yaml target.changed.yaml > target.diff
patch -i target.diff target.yaml

参考: https://github.com/mikefarah/yq/issues/515#issuecomment-830380295

まとめ

今回はArgo Workflowsを使ったKubernetes(EKS)のアップグレードについて紹介させていただきました。
紹介したワークフローはKubernetes1.27とKubernetes1.28のリリースの間で作成したもので、Kubernetes1.28のアップグレードはこのワークフローを使ってアップグレードしました。
これのおかげで面倒だったアップグレード作業を大分楽にすることができました。

明日の記事の担当は早野さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

MLOps基盤のフルマネージド化に向けたVertex AI Pipelinesへの移行

こんにちは。エンジニアの竹田です。
BUYMAの検索システムやMLOps基盤の開発・運用を担当しております。

こちらはEnigmo Advent Calendar 2023の21日目の記事です 🎄

弊社では2021年頃よりMLOps基盤をGoogle Cloud PlatformのAI Platform Pipelines上に構築して開発・運用を行っています。
この度、Vertex AI Pipelinesへの移行を全面的に進めることになりましたので、ご紹介も兼ねて記事にしたいと思います。

背景

2024/07にAI Platform Pipelinesが非推奨になるという通知を受けたことがきっかけです。
AI Platform Pipelines deprecations  |  Google Cloud

非推奨の通知が移行を開始するトリガーではあったものの、かねてからGKEクラスタの運用をどうにかできないかなと考えていました。
Vertex AI Pipelinesへの移行によりフルマネージド化できるのは大きなモチベーションとなっています。

移行における課題

この機会にKubeflow Pipelines SDK(以下、kfp) v1からv2へのアップグレードを進めています。
移行ドキュメントも提供されているため、それほど苦労はしないものと考えておりました。
が、結果としてこの選択が多くの苦労を抱えることになってしまいました 😢
実際に kfp v2 利用してみて、良かった点・苦労している点を交えてご紹介いたします。

※下位互換で動作させることも可能でしたが、kfp v1での機能拡張は行われない、そのうち書き換えが必要になる、という点を考慮してkfp v2への書き換えに踏み切りました。

kfp v1からv2へ

kfpは、kubernetes上で機械学習パイプラインを動作させるためのツールキットです。
コードベースはPythonです。
An introduction to Kubeflow

パイプラインを作成してVertex AI Pipelines上で動作させると、動作フローが視覚的に分かりやすく表現されます。

Pythonで記述したコードをコンパイルして利用する性質上、kfp v1の頃からかなりクセが強いなとは感じていました。
実際に利用してみた上での良い点、苦労している点を列挙し、所感を書いていこうと思います。

kfp v2の利用バージョン

# pip list | grep kfp
kfp                              2.3.0
kfp-pipeline-spec                0.2.2
kfp-server-api                   2.0.3

kfp v2の良い点

  • 入出力に利用するInput[xxx] / Output[xxx] が便利
  • ParallelForによる並列処理の結果をCollectedで受け取れるようになった
  • @dsl.componentset_accelerator_typeが直感的
入出力に利用するInput[xxx] / Output[xxx] が便利

kfp v2では入出力に利用するオブジェクトがこの形(基本的に Input[Artifact] / Output[Artifact] の利用)にほぼ統一されており、GCS上のパスを意識せず利用できるため非常に便利です。
https://www.kubeflow.org/docs/components/pipelines/v2/data-types/artifacts/

ParallelForによる並列処理の結果をCollectedで受け取れるようになった

kfp v1でもParallelForは利用できましたが、fan-in(複数の入力を一つにまとめること)が厄介でした。
kfp v2では最近になってCollectedが利用可能となり、ParallelForの後に呼び出すことで結果をリスト形式でfan-inできるため、コードの可読性も飛躍的に向上します。

@dsl.componentset_accelerator_typeが直感的

個人的な好みの部類かもしれませんが、kfp v2は定義周りが直感的になった印象があります。
https://www.kubeflow.org/docs/components/pipelines/v2/migration/#create_component_from_func-and-func_to_container_op-support

kfp v2で苦労している点

  • 変数展開がおかしくなることがある
  • 型指定の厳密化により、何を渡せばよいのか分からなくなることがある
変数展開がおかしくなることがある

kfp v2への移行で最も困っている点です。以下のようなissueも挙がっています。
https://github.com/kubeflow/pipelines/issues/10261
変数内に何らかの文字列や数値を入れているはずが、実際に利用する場合に以下のような展開がされてしまいます。
{{channel:task=;name=g;type=String;}}
コンポーネントの出力結果をうまく展開できない場合は以下のような内容です。 {{channel:task=term-calc;name=list_date;type=typing.List[str];}}
機械学習の初回実行プロセスの多くがBigQueryからのデータ取得を行っており、データ取得期間や特徴量を変数で管理しているため、既存のパイプラインコードではPythonのformatメソッドによる書式変換を多用しています。
この書式変換のほとんどが正常に動かなくなってしまい、試行錯誤を繰り返すことになってしまいました。
以下、期待した変数展開とならずにエラーとなるパターンの一部です。

パイプライン引数やコンポーネントの返却結果をdictに加えてコンポーネントに渡した場合

@dsl.component
def convert_str(tmpl: str, value: dict, output: Output[Artifact]):
    with open(output.path, "w") as f:
        f.write(tmpl.format(value))
  
@dsl.pipeline(name="test", description="test prediction")
def test_pipeline(table_name: str = "sample"):
    value = {
        "table_name": table_name
    }
    sql = "SELECT * FROM {0[table_name]}"
    convert_str_op = convert_str(tmpl=sql, value=value)

コンパイル時のエラー内容

ValueError: Value must be one of the following types: str, int, float, bool, dict, and list. Got: "{{channel:task=;name=table_name;type=String;}}" of type "<class 'kfp.dsl.pipeline_channel.PipelineParameterChannel'>".

パイプライン引数をパイプライン本体で利用しようとした場合

@dsl.pipeline(name="test", description="test prediction")
def test_pipeline(periods: str = "1m"):
    target_file = f"periods_{periods}.yaml"

    with open(target_file, mode="r") as f:
        periods_conf = yaml.safe_load(f)

コンパイル時のエラー内容

FileNotFoundError: [Errno 2] No such file or directory: 'periods_{{channel:task=;name=periods;type=String;}}.yaml'
型指定の厳密化により、何を渡せばよいのか分からなくなることがある

パイプライン引数を利用しようとして以下のようなエラーが出たり

TypeError: PipelineParameterChannel is not a parameter type.

特定コンポーネントにinputを渡した場合に以下のようなエラーが出たり

ValueError: Constant argument inputs must be one of type ['String', 'Integer', 'Float', 'Boolean', 'List', 'Dict'] Got: <kfp.dsl.pipeline_task.PipelineTask object at 0x7f8a03f89880> of type <class 'kfp.dsl.pipeline_task.PipelineTask'>.

といったことが割と発生します。
自分としては正しい型での引き渡し、および参照をしているつもりのため、どう対処してよいか分からなくなることが多いです。
事前にキャストすることで正常に動作することもあれば、そもそもデータ型の扱いを見直す必要があったりします。

所感

上述の苦労している点での引っ掛かりが多いのが難点で、残念ながら使いやすさは感じられていません。
ですが、一度形としてできてしまえばテンプレート化できると思われるため試行錯誤しながら進めている、というのが現状です。

コンテナイメージ化しているコンポーネント内部の挙動はほぼ変更なしで動作しており、ほとんどが「変数展開がおかしくなる」部分の障壁により思うように進捗していないといった状態です。
具体的にこうすれば良い、といったアプローチが見つけられたら何かしらの形で記事にできればと考えております。

引き続き、Vertex AI Pipelines移行による機械学習基盤のフルマネージド化を目指して邁進していく所存です 💪

おわりに

明日の記事の担当はインフラチームの福田さんです。EKS周りのお話です。お楽しみに!!


株式会社エニグモ すべての求人一覧

hrmos.co

外部キー制約が使えない場合のRailsの実装方法

こんにちは、エンジニアの川本です。
主にBUYMAの決済・配送を担当しているチームでバックエンドの開発をしています。

この記事は Enigmo Advent Calendar 2023 の 20 日目の記事です。

個人開発でPlanetScaleというMySQL互換のサーバーレスデータベースを使用しているのですが、特筆すべき仕様として外部キーのサポートがありません。

planetscale.com

外部キー制約はDBレベルで強い整合性を担保できる便利な手段ですが、PlanetScaleではその機能が利用できないので、アプリケーションレベルで整合性を担保する必要があります。

MySQLの外部キーのオプションにはいくつか種類がありますが、これらが使えない場合にアプリケーション側ではどのように担保すればよいのでしょうか?

今回は、Railsを例にしてアプリケーション側でMySQLの外部キーに相当する機能をどのように担保できるのかを検証してみようと思います。

余談:PlanetScaleについて

最近PlanetScaleはベータ版で外部キーをサポートし始めましたが、残念ながらHobbyプランではまだサポートされておりません。

PlanetScaleの基盤であるVitessはOnline DDLの機能を提供しており、それが原因で外部キーのサポートが長らく難しかったようです。

以下のドキュメントやブログには、PlanetScaleが外部キーをサポートできるようになるまでの背景や課題、そしてその克服方法についての詳細な情報が記載されています。興味がある方はぜひ読んでみてください。

外部キーのサポートが難しかった理由

外部キーをサポートするための取り組み

親子関係のテーブルを作成

まず親子関係にある、Parent, Childテーブルを作成してサンプルデータを入れる。

-- テーブル作成
mysql> CREATE TABLE parent (
    ->     id INT NOT NULL,
    ->     PRIMARY KEY (id)
    -> ) ENGINE=INNODB;
mysql> CREATE TABLE child (
    ->     id INT NOT NULL,
    ->     parent_id INT NOT NULL,
    ->     PRIMARY KEY (id)
    -> ) ENGINE=INNODB;
    
-- テストデータをインサート
mysql> INSERT INTO parent (id) VALUES (1), (2);
mysql> INSERT INTO child (id, parent_id) VALUES (1, 1), (2, 1), (3, 2);

-- データ構造の確認
mysql> SELECT * FROM parent p JOIN child c ON p.id = c.parent_id;
+----+----+-----------+
| id | id | parent_id |
+----+----+-----------+
|  1 |  1 |         1 |
|  1 |  2 |         1 |
|  2 |  3 |         2 |
+----+----+-----------+

MySQLの外部キー制約

MySQLでは以下4つのON DELETE副次句で指定できる参照アクションがあります。 ON UPDATE副次句もありますが、今回はON DELETEに限定することにします。

dev.mysql.com

ON DELETE CASCADE

親テーブルから行を削除し、子テーブル内の一致する行を自動的に削除する。

-- ON DELETE CASCADEを指定して外部キー制約を設定
mysql> ALTER TABLE child
    -> ADD CONSTRAINT fk_parent
    -> FOREIGN KEY (parent_id)
    -> REFERENCES parent(id)
    -> ON DELETE CASCADE;

-- parentのid = 1のレコードを削除する
mysql> DELETE FROM parent WHERE id = 1;

-- parent_id = 1のchildのレコードも削除されていることを確認できる
mysql> SELECT * FROM child;
+----+-----------+
| id | parent_id |
+----+-----------+
|  3 |         2 |
+----+-----------+

ON DELETE SET NULL

親テーブルから行を削除し、子テーブルの外部キーカラムをNULLにする。
※ この設定をするときは、childparent_idNOT NULLにしない。

-- ON DELETE SET NULLを指定して外部キー制約を設定
mysql> ALTER TABLE child
    -> ADD CONSTRAINT fk_parent
    -> FOREIGN KEY (parent_id)
    -> REFERENCES parent(id)
    -> ON DELETE SET NULL;

-- parentのid = 1のレコードを削除する
mysql> DELETE FROM parent WHERE id = 1;

-- parent_id = 1のchildのレコードのparent_idはNULLになっていることを確認
mysql> SELECT * FROM child;
+----+-----------+
| id | parent_id |
+----+-----------+
|  1 |      NULL |
|  2 |      NULL |
|  3 |         2 |
+----+-----------+

ON DELETE RESTRICT or ON DELETE NO ACTION or 指定なし

親テーブルに対する削除操作は拒否されます。また、ON DELETE RESTRICT or ON DELETE NO ACTION or ON DELETE 指定なしは同じ挙動になります。以下の例ではON DELETE 指定なしで例を示します。

-- ON DELETE指定なしで外部キー制約を設定
mysql> ALTER TABLE child
    -> ADD CONSTRAINT fk_parent
    -> FOREIGN KEY (parent_id)
    -> REFERENCES parent(id)
    
-- parentのid = 1のレコードを削除する
-- childにはparent_id = 1のレコードがあるので削除拒否される
mysql> DELETE FROM parent WHERE id = 1;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`myapp_development`.`child`, CONSTRAINT `fk_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`))

-- parentもchildも削除されていない
mysql> SELECT * FROM parent p JOIN child c ON p.id = c.parent_id;
+----+----+-----------+
| id | id | parent_id |
+----+----+-----------+
|  1 |  1 |         1 |
|  1 |  2 |         1 |
|  2 |  3 |         2 |
+----+----+-----------+

Rails側の実装方法

Railsでは、Active Recordのdependentオプションを使用して、MySQLの外部キー制約に相当する機能を実現できます。

dependentオプションは親レコードに対してActiveRecord::Persistence#destroyが実行されたときに、紐ずいている子レコードに対して実行されるメソッドのことです。

ON DELETE CASCADE

ON DELETE CASCADEに相当することは、delete_all, destory, destory_asyncのいずれかで実現することができます。これら3つは全て最終的に実現できることは同じですが、それぞれで以下のように挙動の違いがあります。

delete_all

delete_allは、parentに関連付けられたchildが一括で1つのSQLで削除します。

また、childに対してActiveRecord::Persistence#deleteが実行されるので、ActiveRecord::Persistence#destroy実行時に作用するbefore_destroyやafter_destroyといったコールバックや孫クラスのdependentオプションが実行されません。

そのため、単純に削除SQLを実行するだけなので関連するchildが多い場合にはdestroyよりパフォーマンスが向上する可能性があリます。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :delete_all
end
irb(main):002> parent = Parent.find(1)
irb(main):054> parent.destroy
  TRANSACTION (0.7ms)  BEGIN
  Child Delete All (1.2ms)  DELETE FROM `child` WHERE `child`.`parent_id` = 1
  Parent Destroy (0.7ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (1.9ms)  COMMIT
=> #<Parent:0x0000ffffaf032050 id: 1>

destroy

destroyは、parentに紐づくchildを全て取得して1件ずつ削除します。

ActiveRecord::Persistence#destroyが実行されるため、before_destroyやafter_destroyなどのコールバックも実行され、孫クラスにあるdependentオプションも実行されます。

そのため、関連するchildが多いと発行されるSQLも増え、コールバックの実行や孫クラスのdependentオプションの実行が多くなり、delete_allよりもパフォーマンスが低下する可能性があります。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :destroy
end
irb(main):002> parent = Parent.find(1)
irb(main):062> parent.destroy
  TRANSACTION (0.3ms)  BEGIN
  Child Load (1.0ms)  SELECT `child`.* FROM `child` WHERE `child`.`parent_id` = 1
  Child Destroy (1.3ms)  DELETE FROM `child` WHERE `child`.`id` = 1
  Child Destroy (1.1ms)  DELETE FROM `child` WHERE `child`.`id` = 2
  Parent Destroy (0.9ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (1.2ms)  COMMIT
=> #<Parent:0x0000ffffafdad888 id: 1>

destroy_async

destroy_asyncは、parentに関連する全てのchildを非同期で1件ずつ削除します。

紐づくchildが非常に多く、即時での削除を求められない場合に有効です。紐づくchildが多いと処理が最悪の場合はタイムアウトする可能性もあります。そのような場合、まずparentを削除してクライアントにレスポンスを速やかに返し、残りの紐づくchildは非同期で削除することで問題を解決できます。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :destroy_async
end
irb(main):002> parent = Parent.find(1)
irb(main):070> parent.destroy
  TRANSACTION (0.3ms)  BEGIN
  Child Load (0.8ms)  SELECT `child`.* FROM `child` WHERE `child`.`parent_id` = 1
  Parent Destroy (0.8ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (2.0ms)  COMMIT
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 63fc4528-934a-405c-9311-7bee9fb706b1) to Async(default) with arguments: {:owner_model_name=>"Parent", :owner_id=>1, :association_class=>"Child", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
=> #<Parent:0x0000ffffae761700 id: 1>
irb(main):071> Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 63fc4528-934a-405c-9311-7bee9fb706b1) from Async(default) enqueued at 2023-12-16T09:16:43Z with arguments: {:owner_model_name=>"Parent", :owner_id=>1, :association_class=>"Child", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
  Parent Load (3.0ms)  SELECT `parent`.* FROM `parent` WHERE `parent`.`id` = 1 LIMIT 1
  Child Load (5.1ms)  SELECT `child`.* FROM `child` WHERE `child`.`id` IN (1, 2) ORDER BY `child`.`id` ASC LIMIT 1000
  TRANSACTION (0.3ms)  BEGIN
  Child Destroy (0.9ms)  DELETE FROM `child` WHERE `child`.`id` = 1
  TRANSACTION (2.0ms)  COMMIT
  TRANSACTION (0.3ms)  BEGIN
  Child Destroy (1.0ms)  DELETE FROM `child` WHERE `child`.`id` = 2
  TRANSACTION (1.7ms)  COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 63fc4528-934a-405c-9311-7bee9fb706b1) from Async(default) in 63.81ms

ON DELETE SET NULL

nullify

ON DELETE SET NULLに相当することはnullifyで実現できます。

parentに紐づくchildのparent_idをnullに更新して、parentを削除しています。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :nullify
end
irb(main):088> parent = Parent.find(1)
irb(main):090> parent.destroy
  TRANSACTION (0.3ms)  BEGIN
  Child Update All (5.0ms)  UPDATE `child` SET `child`.`parent_id` = NULL WHERE `child`.`parent_id` = 1
  Parent Destroy (3.3ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (1.3ms)  COMMIT
=> #<Parent:0x0000ffffae66f9a0 id: 1>

ON DELETE RESTRICT or ON DELETE NO ACTION or 指定なし

ON DELETE RESTRICTまたはON DELETE NO ACTIONに相当することは、 restrict_with_exceptionまたはrestrict_with_errorのいずれかで実現することができます。これら2つは全て最終的に実現できることは同じですが、それぞれで以下のように挙動の違いがあります。

restrict_with_exception

parentに紐づくchildが存在することを確認して、処理をロールバックしてActiveRecord::DeleteRestrictionErrorという例外を発生させます。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :restrict_with_exception
end
irb(main):088> parent = Parent.find(1)
irb(main):094> parent.destroy
  TRANSACTION (0.6ms)  BEGIN
  Child Exists? (1.0ms)  SELECT 1 AS one FROM `child` WHERE `child`.`parent_id` = 1 LIMIT 1
  TRANSACTION (0.5ms)  ROLLBACK
/usr/local/bundle/gems/activerecord-7.0.8/lib/active_record/associations/has_many_association.rb:16:in `handle_dependency': Cannot delete record because of dependent child (ActiveRecord::DeleteRestrictionError)

restrict_with_error

parentに紐づくchildが存在することを確認して、処理をロールバックしてfalseを返します。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :restrict_with_error
end
irb(main):088> parent = Parent.find(1)
irb(main):098> parent.destroy
  TRANSACTION (0.5ms)  BEGIN
  Child Exists? (0.6ms)  SELECT 1 AS one FROM `child` WHERE `child`.`parent_id` = 1 LIMIT 1
  TRANSACTION (0.4ms)  ROLLBACK
=> false

最後に

ここまでの紹介で、RailsアプリケーションでMySQLの外部キー制約の参照アクションを実現する手段が理解できました。

ただし、データ整合性が担保されるのは、外部キー制約に準拠したアプリケーションからの実行時に限られます。もし、同じDBを参照するが外部キー制約に準拠していないアプリケーションが存在する場合、どのような影響が生じますでしょうか?

外部キー制約のないアプリケーションからの実行により、データ整合性が維持されなくなる可能性があります。このような事態を避けるためには、できるだけDBレベルで整合性を担保する方が望ましいです。

Planet Scaleのような外部キー制約をサポートしていないDBでは、今回紹介したようなアプリケーションの実装が有効であるかもしれません。しかし、外部キー制約がサポートされているDBでは、DBレベルでの制御が安全であると言えるでしょう。

明日の記事担当はデータエンジニアリングチームです!お楽しみに!

株式会社エニグモ すべての求人一覧

hrmos.co

エニグモにおける開発生産性分析の取り組み

こんにちは、サービスエンジニアリング本部の平井です。

こちらはEnigmo Advent Calendar 202320日目の記事です。

私は、エンジニア部門で取り組んでいる開発生産性分析について紹介します。

開発生産性分析を試みた経緯

現在、エニグモでは開発組織体制の変更、メンバー増強など様々な組織強化を目指した動きが加速してきています。ただ、そのような施策が開発組織のパフォーマンスを向上させているのか定量的な指標で測ることができませんでした。 また、開発組織としては、開発を通して一定のスピードでユーザーに十分な価値を届ける責任を負っているものの、開発スピードを測る良い方法がありませんでした。 このように組織パフォーマンス向上施策の結果を確認するため、開発組織としての開発スピードを図るために開発生産性分析の取り組みが始まりました。

また、この取り組みは開発生産性に興味がある有志のメンバーが集まり進めていきました。

指標選定

分析する指標としては、Googleが提案しているFour Keysを参考にしました。 Four Keysを参考にした理由は以下になります。

  • 開発スピードを測る指標が含まれている。
  • 自分達で計測、分析の基盤を整えられそう。
  • 一般的な指標である。

そして、 指標の中で速度に関わる指標であり、計測が容易そうな変更のリードタイムデプロイ頻度を計測することが決まりました。 デプロイ頻度に関しては、営業日や開発メンバーの増減による影響を少なくするために、 @hiroki_daichiさんが紹介されていたd/d/dというやり方で 1日あたりの1開発者あたりのデプロイ回数を計算することにしました。

また、現在BUYMAは基幹システムと複数のマイクロサービスで構成されていますが、どの開発チームも修正することが多い基幹システムにおけるこれらの指標を計測していくことになりました。

各指標の定義

前提として、BUYMAの基幹システムはGitlabをホスティングサービスとして利用しています。 本番環境へのデプロイは内製アプリケーションを通して行われ、開発者が各々デプロイ依頼を作成します。 本番環境へのデプロイプロセスは1日3回実行されるため、各開発者は作成した依頼をそのどれかのプロセスに乗せて本番化します。

Four Keysを参考にしつつ、このようなBUYMAの特徴を考慮して各指標の定義をチームで相談して決めました。

その上でリードタイムは以下の定義で計算しています。

MR内の最初のコミットからそのMRが本番環境に反映されるまでの時間

各開発者によって開発手法は異なるため最初のコミットタイミングが微妙にずれる可能性はありますが、 開発 -> レビュー -> QA -> デプロイという一連の開発プロセスを計測できるためこのような定義にしました。

d/d/dに関しては、デプロイ回数/営業日/開発者数となっていて、それぞれ以下のような定義になっています。

デプロイ回数: 本番化された本番化依頼の数

BUYMAの本番化の特徴として、1デプロイに複数の修正が含まれるためその一つ一つの修正を個別に数えたかったためこのような定義にしました。

営業日: 土日祝日を抜いた平日

営業日は特に一般的に使われているものです。

開発者数: マージされたMRに参加したユニークGitLabユーザー数

エンジニアリング部門に属していても「基幹システムに関わっていない人は含めたくない」、「過去の特定期間の開発者数も出したい」など考慮する点が多く難しかったのですが、「MRに関わった人数は開発に関わる人数と等しいだろう」という考えのもとチームで相談してこのような定義になりました。

データ収集、可視化の仕組み

次に、指標の計測と可視化の仕組みについて説明します。

Airflowにワークフローを構築して、MRに関わる情報をGitLabのAPIから収集しBigQueryに格納しています。 デプロイに関する情報は内製本番化アプリケーションが利用しているデータベースに永続化されているため、BigQueryに連携してGitLabの情報とJOINできるようにしました。 可視化に関しては、BigQuery上のレコードをSQLで加工してLookerのダッシュボードを使うことで実現しています。

開発性生産性分析システム構成図

リードタイムに必要なデータ収集

リードタイムを計測するために以下のGitLab APIを利用しています。 基本的にはAPIのレスポンスから日次の差分データを取得して、そのままBigQueryのテーブルに保存しています。

List all projects API

GitLabのプロジェクトのマスターデータを収集するために利用しています。 収集したデータはLookerで可視化する際にプロジェクト名を表示するなどに利用しています。

List group merge requests API

MRデータを収集するために利用しています。 マージ済みMRの情報のみ必要なのでAPIパラメータを使ってマージ済みMRの情報のみ収集しています。

Get single merge request commits API

MRに紐づくコミットの情報を収集するために利用しています。 ここで取得したデータを利用してMRに紐づく最初のコミットを特定して、リードタイムを計算します。

d/d/d に必要なデータ収集

リードタイムを計測するために以下のGitLab APIを利用しています。 こちらも日次の差分データをそのままBigQueryのテーブルに保存しています。

List a Project’s visible events API

GitLabのイベント情報を収集するために利用しています。 マージされたMRに参加したユニークGitLabユーザー数を計算する際にここで収集したデータを利用しています。

可視化について

先述したように可視化にはLookerのダッシュボードを利用しています。 LookMLSQLを組み立てて指標を計算しています。 エニグモではBUYMAをメインの機能で分割して開発チームを組織していて、それらをドメインチームと呼んでいます。指標の監視や分析は各ドメインチームにやってもらっているため各ドメインチーム毎にダッシュボードを作成しています。

可視化したあるチームのリードタイム

運用について

指標の監視や分析は、細かいやり方を指定せずにドメインチームに依頼しています。 例えば私が所属しているチームでは毎週の振り返り時にリードタイムとd/d/dを確認して、何か問題があれば改善案を考えるという運用をしています。 また、開発生産性指標の計測にあたり、 開発者にMRへのラベル付与を依頼しました。MRに付与されたラベル情報をもとにドメインチーム毎のリードタイムを計測しています。

今後の課題

今後の課題としては以下になります。

  • BUYMA基幹システム以外でまだデータ収集できていないマイクロサービスがあるため正確に開発組織全体の生産性を測れていない。
  • 要件定義、仕様決定などのディスカバリーフェーズにかかっている時間を図れていない。
  • 変更障害率、サービス復元時間など安定性の指標を図れてない。
    • 現状だとリードタイムやd/d/dが向上した際に、それが障害発生による細かい修正が増えたのが原因なのかわからない。

終わりに

今回はエンジニア部門で取り組んでいる開発生産性分析について紹介しました。自分自身、データ収集処理の開発、可視化を担当し、自分達の開発活動が定量データとして表現される面白さを感じました。 最後までご覧頂きありがとうございました。明日の記事の担当は検索エンジニアの竹田さんです。お楽しみに。

現在、エニグモではこのような開発生産性に関わる取り組みを行っています。 興味のある方は以下の求人をご参照ください!!

株式会社エニグモ すべての求人一覧

hrmos.co

マルチカルチャーな職場におけるコミュニケーション

こんにちは、グローバルチームのエンジニアのFernandです。

この記事はEnigmo Advent Calendar 2023の19日目の記事です。私は、15年近くフルスタックエンジニアとして働いてきました。さまざまなプロジェクトに関わってきましたが、、新しいチームに参加するたびに、技術的なスキルだけでなく、特にソフトスキルについても新しく学ぶことが多くあります。今回は、職場のミーティングで外国人として直面した課題を共有したいと思います。

ある日、ITエンジニアの妻が彼に頼んで、「スーパーで牛乳を1パック買ってきて。もし卵があれば1ダース買ってきて」と言いました。そこでエンジニアは買い物に行き、言われた通りに行動し、帰宅しました。手には牛乳パックが12個ありました。
妻は驚愕しました。何が間違いだったのでしょうか?


プロジェクト・マネジメント協会(PMI)の報告によると、ビジネス目標を達成しないプロジェクトの半数以上は、効果的なコミュニケーションの欠如によるものとされています。実際に、不十分なコミュニケーションにより、1500億円を投じたプロジェクトの場合には約100億円がリスクとなるのです。ビジネスでは、12個の牛乳パックが大きな損害を生み出してしまうかもしれないのです。
コミュニケーションの齟齬による経済的リスク
ソース: ©2013 Project Management Institute, Inc. Pulse of the Profession In-Depth Report: The High Cost of Low Performance: The Essential Role of Communications, May 2013. PMI.org/Pulse
私が日本の企業で働き始めた当初、さまざまなチームとのコミュニケーションに苦労しました。私と同じように、日本に住む外国人の多くが直面する経験です。
外国人が日本で働くうえでぶつかる壁(言語の壁が半数以上)
ソース:NikkeiAsia
私が最初に入社したスタートアップ企業では、お客様の満足度が最も重要であり、彼らを神様のように対応すべきということを教わりました。そのため、ミーティングに参加する際、私は静かに座り、より理解するために一層耳を傾ける必要がありました。私は日本語に自信がなかったため、質問をすることや間違いを犯すことを恐れていました。自分が話すことで、クライアントから私たちの会社がネガティブに評価されるのではないかという不安がありました。
しかしそれでも、各ミーティングの後に反省点を見直して、気づいたことを改善してから自信を持って質問をするようにしました。そして、効果的なコミュニケーションを大事にする日本のチームに参加する機会を得たおかげで、私はミーティング中に質問をする勇気を持ち、プロジェクトやクライアントの要件をより理解できるようになりました。日本人の同僚は忍耐と理解力をもって複雑な概念を簡素化するために時間をかけて説明を繰り返してくれました。
しかし、言語の壁は、誤解を招く障害となることもあることも私は理解しています。それにより、会話の流れが乱れ、自分の意見を正確に表現することが難しくなります。思いを正確に表現することへの取り組みは、話す人と聞く人の双方にとっても苦悩の源となっています。できるだけこれを避けるために、事前に読み込んで準備し、質問をまとめて整理することが大事です。
外国人のメンバーがミーティングをする場合、一人だけに発言の責任が押し付けられないように、協力し合いながら取り組みます。通常、そこでは、生産的に結果を出すために、話の途中で割り込むことが歓迎され、許容されます。曖昧さを解消するために、アイデアを効果的に共有して質問をするこの戦略は時に混沌としたものになりますが、コミュニケーションの理解を促進し、より協力的で生産的な働き方を促進するのに非常に効果的だと感じています。誤解や心配事が生じた際に即座にそれらを明確化することは、コミュニケーションの質を向上させ、より協力的かつ生産的な職場環境を生み出します。参加者全員が議論に貢献することを促すことで、創造性が高まり、アイデアが高められ、より豊かでダイナミックな成果が生まれます。
しかし、文化的な習慣やエチケットにも配慮することが重要です、特に話し手が日本人の場合です。このような場合、割り込むことは失礼とされ、質問はミーティングの最後まで取っておくことが通例です。そういったミーティングでは、私はより受動的なアプローチを取る傾向があります。ただし、質問や懸念事項を早急に解決させたい場合、日本語の能力に自信がなくても話すように努めています。日本語の能力に自信がなくても、ただ聞くだけに頼るのではなく、積極的な参加こそが助けを求めて理解を深める唯一の方法であることを心に留めています。言語の壁を乗り越え、異文化間のコミュニケーションの複雑さをより効果的に乗り越えるためには、日本語の勉強を続けることが重要です。
私は現在、さまざまな国のメンバーから成るチームに所属しています。私たちの主要なコミュニケーション手段は日本語ですが、必要に応じて英語も使用しています。さまざまな時差の課題にも取り組んでおり、私たちのマネージャーはその違いがミーティングのスケジュールに影響しないよう柔軟に対応してくださいます。彼女は流暢な英語と日本語を活かし、私たちの質問や懸念事項をサポートし、チーム内の円滑かつ効果的なコミュニケーションを容易にしてくれます。私は定期的に他のチームとも交流しなければなりません。日本語を使ってのコミュニケーションは難しいですが、貴重な機会であり、コミュニケーションスキルを実践し向上させるチャンスでもあります。しかし、通常業務に追われて、学習に費やす時間の確保がなかなか難しくなっています。プライベートと仕事、そして言語学習のバランスを取ることは大変な努力を要します。まるでジャグリングをするようなもので、集中して言語の練習だけに時間を割くことはなかなか難しいものです。
言語の不完全さや課題があっても、ミーティングでの同僚たちはそれを判断することはありません。むしろ、言語の壁にとらわれず、積極的に参加して貢献する姿勢は評価されるでしょう。しかし、質問をする際には、ただ質問を投げる、自己アピールのためにするということは避けるべきです。そのような行動は、ビジネス上での評判に悪影響を及ぼすだけでなく、私たちが維持しようとしている協力的な雰囲気にも支障をきたす可能性があります。
さらに、会議を進行する際は、コミュニケーション能力や自信のレベルに関係なく、全参加者が積極的に参加できる包括的で支援的な雰囲気を育むことも重要です。間違った質問や関連性のない質問をする人がいても批判するべきではないでしょう。そのような場合に他者を批判すると、プロジェクトが始まる前から失敗したも同然です。前述したように、効果的なコミュニケーションが生産性を向上させるという認識はとても重要です。研究によれば、効果的なコミュニケーションは生産性を最大25%向上させることができます。最終的には、プロジェクトの成功は効果的かつ効率的なコミュニケーション能力にかかっています。卵と牛乳の個数を間違えることもないことでしょう。不確実な点が生じた場合には、積極的に話して明確化することが重要です。積極的なリスナーであり、共感を持って接することを心がけましょう。英語や日本語の流暢さだけに焦点を合わせるのではなく、彼らの質問やアイデアの価値がプロジェクトを前進させ成功させる上でとても重要です。

(English version)
Communication in Workplace

An IT engineer was asked by his wife to go to the store and buy a carton of milk, and if there are eggs, buy a dozen. So the engineer goes shopping, does as she says, and returns home with 12 milk cartons.

Oh boy , But what went wrong?


According to the Project Management Institute (PMI) report, over half of projects that fail to meet business goals can be attributed to ineffective communication. In fact, poor communication puts approximately $75 million at risk for every $1 billion spent on projects. Well, it seems that one dozen cartons of milk can potentially result in a substantial financial loss.

The amount at risk for every US$1 billion spent on a project.
Source: ©2013 Project Management Institute, Inc. Pulse of the Profession In-Depth Report: The High Cost of Low Performance: The Essential Role of Communications, May 2013. PMI.org/Pulse
When I first started my journey working at Japanese companies, I often struggled to communicate effectively with various teams. Unfortunately, this burden is not unique to me but a shared experience among many foreign residents in Japan.
Barriers foreigners encounter when working in Japan. Language barrier is the most prominent obstacle.
Source:NikkeiAsia
Working in startup companies, I quickly learned that client satisfaction was held in the highest regard, often considering them as king or god (o-kyaku-sama wa kamisama desu) whose every request must be fulfilled. Therefore, when I joined meetings, I had to sit quietly and listen harder to understand the discussions. I feared asking questions and making mistakes because of my Japanese language proficiency. My company might be judged by the client whenever I attempt to speak up. I was able to manage this by researching sample implementations after each meeting and creating prototypes before confidently raising a question.
Then, I had the opportunity to join a Japanese team that fostered effective communication. Encouraged by my supportive environment, I mustered the courage to ask questions during meetings, improving my comprehension of projects and clients' requirements. My Japanese colleagues displayed immense patience and understanding, taking the time to reiterate their explanations and simplify complex concepts, ensuring everyone understood the information.
But don't get me wrong, despite the positive environment, the language barrier often presents obstacles that could lead to misunderstandings and misinterpretations. It disrupts the conversation flow and makes it challenging to express oneself fully. The struggle to articulate thoughts accurately has become a source of frustration for both the speaker and the listener. In order to avoid this as much as possible, I read and prepared beforehand, collating and organizing my questions.
In situations where the speaker is a non-Japanese teammate, I collaborated, ensuring that the onus of speaking does not solely rest on one person. Usually, in meetings that consist of non-Japanese participants, interruptions are welcomed and accepted to foster a productive environment where ideas can be effectively shared, questions can be asked, and clarification can be provided. While this strategy may seem chaotic at times, I find that it is incredibly effective in facilitating comprehension and generating deeper insights. Obtaining immediate clarity on any misunderstandings or concerns enhances the quality of communication and promotes a more collaborative and productive work environment. By establishing a culture where all participants are encouraged to contribute to the discussion, creativity surges, and ideas are elevated, creating a richer and more dynamic outcome.
However, I am also mindful of cultural norms and etiquette, particularly when the speaker is Japanese. In these situations, it would be considered rude to interrupt, and it is customary to reserve questions until the end of the meeting. I tend to adopt a more passive approach during such meetings. Nevertheless, if I do have pressing clarifications or concerns, I try to speak up regardless of my lack of confidence in my Japanese proficiency. I remind myself that the only way to seek assistance and gain a better understanding is by actively participating in the conversation rather than solely relying on attentive listening. However, continuing to study Japanese remains paramount in overcoming these language barriers and navigating the intricacies of cross-cultural communication more effectively.
Currently, I am fortunate to be a part of a diverse team with members from different parts of the world. Although our primary mode of communication is Japanese, we also embrace the use of English when needed. Despite the challenges of various time zones, our manager has done an exceptional job ensuring that these differences do not impede our meeting schedules. With her fluency in English and Japanese, she adeptly accommodates our questions and concerns, facilitating smooth and effective communication within the team. I also have to interact with other teams periodically. Communicating using the Japanese language is challenging, but it also gives me valuable exposure and the chance to practice and enhance my communication skills. Though I have to admit that the demanding nature of daily life and work commitments has restricted the amount of time available for study. Balancing work, personal responsibilities and language learning can be a juggling act, making it quite challenging to allocate uninterrupted time for focused language practice.
Ultimately, the success of our projects hinges on our ability to communicate efficiently and effectively. If uncertainty arises, it is crucial to speak up and seek clarification. Rest assured that in meetings, colleagues will not judge any language imperfections or challenges you may encounter while expressing yourself in Japanese or English. Instead, your willingness to participate and contribute despite any linguistic limitations will be appreciated and acknowledged. However, it is also essential to avoid asking questions solely for the sake of asking or to showcase your skills. Engaging in such behavior may have a detrimental impact on your professional reputation and hinder the collaborative atmosphere we strive to maintain.
In addition, it is equally important for the meeting host to take accountability in fostering an inclusive and supportive meeting environment that encourages active participation from all attendees, regardless of their communication skills or confidence levels. It is critical to refrain from criticizing individuals who may ask questions that might seem incorrect or irrelevant. Criticizing others in such instances, the project had already failed even before it started. Be an active listener and practice empathy. Fluency in English or Japanese should not be the sole focus; instead, the value of their questions and ideas truly matters in moving the project forward and achieving success.

明日の記事の担当はSellチームの平井さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

細かいことの組み合わせで日々の開発を快適にする

こんにちは、iOSエンジニアの池田です。

この記事は Enigmo Advent Calendar 2023 の 18 日目の記事です。 この記事では担当のプロジェクトにおいて日々の開発を快適にするために実施している細々したことについて紹介します。

私はBUYMAiOSアプリ開発を担当しています。 iOSアプリ開発に限らずプロジェクトを進める中では、コアタスク(ここではiOSアプリの開発作業と定義)とノンコアタスク(コアタスク以外の作業)が発生します。

開発の効率を上げるため、開発者の開発の快適性を上げるためにノンコアタスクは極力削っていきたいものです。 ここではノンコアタスクの中でもチケット管理、Pull Requestの管理に関わる部分について触れていければと思います。

現在のプロジェクト環境


現在の担当プロジェクト内では、チケット管理にJIRA、開発用のプラットフォームにはGitHubを利用しているので、それらに関わる内容について触れていきます。

また、コミュニケーションツールとしてSlackを利用しているため、こちらとの連携も少し触れます。

JIRA


自動化

JIRAのチケット管理では、各チケットの開始日や終了日、担当者、チケットの作業状況など様々な情報の日々の更新が必要になってきます。

一つ一つの作業は作業時間も短く単純作業ではあるのですが、数が多くなると作業時間ももちろん累積されますし、何にも考えずにできるわけではないため、じわじわ地味に体力を削られます。

そういった作業を減らし、少しでも快適性を上げるためにはJIRAの「自動化」が有効です。

以下は私が担当するプロジェクトで設定している「自動化」のルール一覧です。

様々設定されていますが、この中でも「プルリクエストがマージされる → 課題を完了にする」が結構気に入っています。

Pull Request のコードレビューを受けた後自分でマージする運用をしている場合、このルールが設定されていないと、以下のような手順が必要になります。

  1. ブラウザでGitHubを開く
  2. GitHubの画面上でPull Requestの一覧を開く
  3. 該当のPull Requestを開く
  4. Pull Requestをマージする
  5. ブラウザでJIRAを開く
  6. JIRAのボード等でチケットを完了にトランジションさせる

これらの手順の中で、4.から 5.では別サービスへの切り替えが必要で、6.の作業ではドラッグ&ドロップかオプションメニューからの選択が必要だったりで、キーボードからマウスへの操作の切り替えも余分に必要になります。

4.で作業に満足して 5.以降忘れることもあり、JIRAのカンバン管理者から完了/未完了の問い合わせが来たとしたらコミュニケーションコストもかかります。

これだけでも意外と積み重ねるとコストになるので、設定することによる快適度は上がっている感じがします。

Gitブランチ作成

もう一つJIRAの機能で細かいけどよく使っている機能がチケットのブランチ作成機能です。

以下チケットの画面キャプチャです。

チケットの「開発 > ブランチを作成 > GITで新しいブランチを作成してチェックアウト」の欄で、チケットIDが入ったブランチ名を自動生成してくれます。

また、入力欄の右側のボタンでクリップボードにコピーできるため、CLIでブランチ作成の際にコピー&ペーストでブランチを作成することができます。

こちらを利用することでブランチ名を考える手間や、CLIで入力する手間を省くことができます。

名前を考えるのって決めの問題なのですが、意外と考えるコストがかかる作業だと思っているので個人的にはすごく助かっています。

GitHub


PULL_REQUEST_TEMPLATE.md

Pull Request を作成して開発されている場合、設定しているリポジトリが多いかと思います。

本プロジェクトでは、以下のようなトピックをフォーマット化して記載するようになっており、記載時の手間を少し減らしています。

  • 関連チケット
  • Pull Request でやったこと
  • 懸念点・注意点
  • 相談事項

また、JIRAとGitHubが連携されるようになっているため、チケットのURLを記載するとJIRAのチケットにGitHubのPull Requestが連動するようになっています。

Slack


SlackとJIRA、SlackとGitHubも連携されており、チケットやPull Requestの条件に応じた更新があったタイミングで通知が来るようになっています。

JIRAの通知

JIRAの通知設定は以下のようになっており、コメントやステータスの更新等通知が来るように設定されています。

GitHubの通知

GitHubの通知設定は以下のようになっており、コメントの見落としが少なくなるようにcomments:'channel'の設定をしているところが個別に設定した部分だったかと思います。

まとめ


チケット管理、Pull Requestの管理に関わる部分についてノンコアタスクを削減するため細々やっていることをご紹介しました。

プロジェクトの環境によって合う、合わない部分があったりしますが、見て頂いた方の何かのご参考になれば幸いです。

日々の変化している状況や、気づいたちょっとした不要な手間など、アンテナを高くしつつさらなる改善、日々の開発の快適度を上げていければと思います。

株式会社エニグモ すべての求人一覧

hrmos.co