開発部門のメンタリング体制

こんにちは、テックリードの Steven です。 この記事で開発部門におけるメンタリングの体制を紹介して、学んだことを説明できればと思います。

メンタリングの目的

メンタリングはエンジニアが仕事を通して提供する価値が上がるようにサポートすることだと思います。 技術力を伸ばすのも重要ですが、仕事が全体的にもっと効率よく進むように仕事のやり方を改善するのがポイントです。

調査のやり方、コミュニケーションの取り方、時間管理、振り返り方、作業のタスク分けとスケジュールなど仕事を進める中で必要となるスキルを伸ばすことを目的としています。

メンタリングはティーチングとも違っていて、どうすればいいのかをただ教えるのではなく、あくまでメンティーをサポートして、自分一人で成長できるようにすることです。 何をすればいいのかを指示するのではなく、問題をどうすれば解決できるのか解決策を助言するか、判断しやすくなるようにアクションを提案することに留めます。

もちろん成長要素のない問題が発生した場合、正解が一つしかない場合、わざわざ考えさせる意味がないので、その時に限って答えを教えます。 手取り足取り、全てを細かく説明するのが優しいと感じられることがありますが、それだと、相手が自分で考えることなく、言われたことをそのまま実行するだけなので、成長には繋がりにくいと思います。 メンターがいなくなって、新しい問題が発生したら、解決するには多分苦労するでしょう。 メンタリングはその状況を避けるためにあって、優しさより成長を優先すべきだと思います。

当然ですが、メンタリングは社員全員にではなく、新卒やジュニア限定としています。 それも当人の技術力、仕事の捌き方によって決まるもので、基準値を超えれば、メンタリングを終わりとします。

メンタリングの体制

開発部門でメンタリングを受けるメンティーにメンターが一人付きます。 そのメンターは日々の作業の手助け、成長のためのサポート、作業のレビュー、週次振り返りを担当します。 メンティーのために時間が取れるように、メンターは基本的に一人のメンティーだけを担当します。

メンティーは 1on1 という対面月次振り返りも受けます。 対面相手はリーダー層のエンジニアになります。

それとは別でメンターのメンタリングのための会議も毎月実施しています。 メンターとメンタリングの経験があるエンジニアが集まる MTG です。

メンティーの週次振り返り

毎週メンティーとメンターで振り返り MTG を実施して、一週間の間にメンティーが行った作業と、発生した問題を分析して、改善案を出します。 YWT の形で行っていて、やったこと(Y)、わかったこと(W)、次やること(T)を事前にメンティーに記事にまとめていただいてから、メンターと二人で話し合います。

YWT の記事でまとめるのは作業内容も含まれますが、それよりも仕事の進め方を改善するために取ったアクション(Y)、作業をする中でやり方についてわかった重要なこと(W)、次の振り返りまで仕事の進め方を改善するために取る予定のアクション(T)というのが内容となります。 技術についてわかったことは自然と増えて、メンタリングをそんなに必要とするものでもないので、それで時間を浪費しすぎないように気をつけています。

その記事を確認した上でメンターはメンティーとディスカッションをして、時間の使い方がよくないとか、調査方法が非効率だとか、メンティーが抱えている問題を掘り出して、改善アクションを提案します。 メンターはできるだけ、表面的な問題ではなく、根底にある問題を暴き出すように努めます。

改善アクションが決まれば、口頭で終わらせるのではなく、アクションをしっかりと振り返り記事に記録して、次の振り返りで実施されたかどうか、フォローアップを行います。 アクションがわかりやすくまとめられて、実施される前提でメンターが振る舞えば、メンティーもやる気が出て、アクションが取りやすくなるかと思います。

YWT の例

以下は振り返りに慣れたエンジニアが実際に書いた YWT です。 入社したてのメンティーでこんな風に状況を分析して情報をまとめるのが難しいと思いますが、目指すべき振り返りの形だと思います。

レビュー入力欄改善と検索UIはプロジェクトの名前です。

Y(Yattakoto)

  • レビュー入力欄改善はキリのいいところで一旦打ち切った
  • 検索UIではfigmaや画面仕様を見ながら詳細設計した

W(Wakattakoto)

  • プロジェクトにアサインされたばかりだが画面仕様書の作成を通して実装できるぐらいには仕様を把握することができた
  • 検索UIのチケット1枚あたりの作業量は膨大ではないのですぐ終わってモチベーションが保ちやすい。レビュー入力欄改善のタスクはチケット1つで結構な作業量なのでチケットを細分化する必要があると感じた。
  • コンポーネント作成のタスクに入ったが、cssの適用はしないのででき上がるviewは不完全なものになるのでタスクはどうやったら完了なのかが曖昧なのがわかった

T(Tsugiyarukoto)

  • ペアプロを通してタスクの完了ラインについて把握する

メンティーの 1on1

毎月メンティーとリーダー層のエンジニアの間で 1on1 という対面振り返り MTG も実施しています。 1ヶ月において、行った作業と成長したところ、まだ抱えている問題を確認します。

目的は週次振り返りと同じで、仕事のやり方に対する問題の掘り出しと、改善アクションの提案です。 メンターと違う方が見ることで、メンターが見逃した問題を発見することもできれば、メンターに相談しにくい話も聞けます。

それに加えて、人事考課で設定した半期の目標の進捗も確認して、進捗が悪ければ、アクションプランを提案します。 カウンセリングというストレスチェックも行って、作業量が多すぎるとか、最近成果が出せてないとか、ストレス要素の排除に努めます。

メンターのメンタリング

メンタリングは制度だけではなく、メンターのスキルでもあるので、そのスキルが磨かれるようにメンターのメンタリングも行っています。 初めてメンターになる方の場合、最初のうちはメンティーの週次振り返りに経験者も同伴します。 最初の会はその経験者が仕切ってメンタリングのやり方を見せますが、それ以降はメンターに任せて、サポートに徹します。

それとは別で、毎月現役メンターとメンタリングの経験者が MTG で集まります。 メンターによるメンティーの話を通して、次にメンターが取るべきアクションをディスカッションします。 メンターのやり方に問題があるとわかれば、何に気をつけるべきか、どんな風に問題を解決すべきかと伝えて、アクションを取っていただきます。 軌道修正の意味合いが強くて、あくまで問題があれば指摘をして、それ以外はメンターに判断を任せます。

メンターも月次 1on1 を受けることがあるので、必要に応じてその場面でもメンタリングに対する助言をします。

学んだこと

当然かもしれませんが、メンタリングにおいても明確な目標を持った方がいいです。 メンティーの課題を分析した上で目標を設定して、達成のためにアクションを取っていただいて、改善の経過を計測するのが重要です。 日々発生する細かい問題をスポットで解決するだけではメンタリングの本当の効果は得られず、メンティーの成長に時間がかかってしまいます。

日々の作業の中で発生する表面的な問題を通して、メンティーがかかえている根本的な課題を掘り出すのもポイントです。 特定の機能のソースコードの場所がわからなくて、ずっと悩んでいて作業が進まなかったというのが問題であれば、課題はソースコードの場所を覚えてないのではなく、調査方法に問題があるか、すべきだった相談をしなかったというところにあります。 そこからメンティーとのディスカッションを通してより細かい原因を割り出して(迷惑をかけたくなかったので、相談をしなかったとか)、改善アクションを決めるのが適切だと思います。 ここでソースコードの場所を教えるだけだと、問題は一時的に解決しますが、おそらく違う形でまた発生するので、根本的な解決にはならないです。

どの時点でメンタリングが不要なのかと基準を設けるのもいいでしょう。 試験とまで行かなくても、誰でも確認できる、共通化された基準があると、メンターとしてもメンティーとしてもやりやすくて、やる気も出ると思います。

最後に、メンタリングの目的とやり方はメンターの間でブレることがあるので、認識合わせをしっかりと行った方が安全です。 特にメンタリングの経験が少ない方だと、メンタリングとティーチングの違いを把握しきれないことがあるので、時間を取ってゆっくりと説明した方がいいと思います。 メンタリングで行うことを明文化して、流用できるガイドを用意できると、ベストかもしれません。

終わりに

エニグモはもともと中途採用のみでしたので、メンタリング制度はありませんでしたが、新卒を積極的に採用することとなって、メンタリングの必要が現れて、どうやっていくか色々考える必要がありました。

メンタリングを通して、メンターがメンティーの代わりに仕事をしてしまうとか、表面的な問題しか解決できずメンティーのレベルが上がらないままでいるとか、望んでいたのとは違う結果になってしまうこともあるので、やり方に気をつけるべきです。

メンタリング制度を導入することで新卒の育成だけではなく、開発部門で振り返り文化が根付いて、メンタリングを受けてないメンバーの仕事のやり方もより早く改善するようになったと思います。

エニグモに入った時にまだなかった文化ですが、未開拓地だった場所に色々道路と橋が建てられて、成長の場としてはよりいいところになったと思います。

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

hrmos.co

Sambaのアクセス制御をDNS名で実施するための設定

こんにちは、インフラエンジニア の 加藤(@kuromitsu_ka)です。

今回は、Sambaのアクセス制御をDNS名で実施したので必要だった設定を記載します。

エニグモ社では、令和の今もオンプレミスでSambaを利用した古き良きログの集約サーバが稼働しています。この度、オンプレミスのログサーバと同じ構成のものをAWSに構築することになりました。その際、Sambaのアクセス制御で少し工夫した部分があったので、ログを残します。

エニグモ社のログ集約サーバの仕組み

ログサーバからアプリサーバごとにディレクトリを作成して、Sambaインストール済みのサーバをログサーバからマウントすることでログを集約しています。インフラチームでない開発者は、基本的にサーバへSSHできないので、ログサーバだけアクセスを許可することで、ログを確認できるようにしています。

Sambaのアクセス制御をDNS名で設定する動機

オンプレミスのSambaのアクセス制御は、公開ディレクトリごとにIPセグメントやログサーバのIPを指定してアクセス許可設定しています。オンプレミスのサーバでは、プライベートIPが固定でしたが、AWSでは、サーバが再作成されても大丈夫なように、DNS名でアクセス制御することにしました。

  • オンプレミスでのSambaアクセス許可設定
hosts allow = ログサーバのIPや、本番環境のIPセグメント

AWSでは、SGでアクセス制御をしてもよかったのですが、運用上が面倒なのでSambaの設定でアクセス制御することとなりました。

  • AWSでの理想のSambaアクセス許可設定
hosts allow = ログサーバのホスト名

SambaサーバのDNS名でのアクセス制御に必要だった設定

ログサーバからマウントされるアプリやDBのサーバ(Sambaサーバ側)ではレコード引きを許可する設定と、ホワイトリスト設定が必要で、ログサーバ(Sambaクライアント側)では、ログサーバの逆引きレコードが必要でした。

必要だった設定(Sambaサーバ側)

ホスト名のIPをレコード引きを許可する設定が必要でした。

  • ホスト名検索の許可
dns proxy = yes
hostname lookups = yes
hosts allow = ログサーバのドメイン名

必要な設定(Sambaクライアント側)

Sambaのアクセスには、PTRレコードが必要でした。

  • CloudFormationでログサーバ作成と同時に逆引きレコードも設定できるようにしました。
RecordSetPtr:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: ${PTRレコードホストゾーン}
      Type: PTR
      Name: !Sub
        - ${FourthOctet}.${ThirdOctet}.${SecondOctet}.${FirstOctet}.in-addr.arpa
        - FirstOctet: !Select [0, !Split [ ".", !GetAtt Instance.PrivateIp ]]
          SecondOctet: !Select [1, !Split [ ".", !GetAtt Instance.PrivateIp ]]
          ThirdOctet: !Select [2, !Split [ ".", !GetAtt Instance.PrivateIp ]]
          FourthOctet: !Select [3, !Split [ ".", !GetAtt Instance.PrivateIp ]] 
      ResourceRecords:
        - !Sub
          - ${良きホスト名}
      TTL: 300

デバッグログ

SambaサーバのDNS名でのアクセス制御に、逆引きレコードが必要となった際のログです。

  • PTRレコードがない状態でのアクセス
    Sambaクライアント側からだと弾かれてしまいます。
[root@SambaClient ~]# mount.cifs //${Sambaサーバ}/test test -o vers=3.0,password=,dir_mode=0755 -vvv
mount.cifs kernel mount options: ip=10.195.101.57,unc=\\10.195.101.57\syslog,vers=3.0,dir_mode=0755,user=root,pass=********
mount error(13): Permission denied
Refer to the mount.cifs(8) manual page (e.g. man mount.cifs)
  • Sambaサーバ側のログ
    IPベースで制御していそうでした。「matchname failed on 10.195.100.9」 ※この際、SambaクライアントのIPは10.195.100.9
Denied connection from 10.195.100.9 (10.195.100.9)
[2022/04/11 16:44:43.212303,  0] ../../source3/smbd/server.c:1788(main)
  smbd version 4.10.16 started.
  Copyright Andrew Tridgell and the Samba Team 1992-2019
[2022/04/11 16:44:43.266814,  0] ../../lib/util/become_daemon.c:136(daemon_ready)
  daemon_ready: daemon 'smbd' finished starting up and ready to serve connections
[2022/04/11 16:44:45.916505,  0] ../../lib/util/access.c:365(allow_access)
  Denied connection from ip-10-195-100-9.ap-northeast-1.compute.internal (10.195.100.9)
[2022/04/11 16:45:17.430789,  0] ../../lib/util/access.c:365(allow_access)
  Denied connection from ip-10-195-100-9.ap-northeast-1.compute.internal (10.195.100.9)
  • 逆引きレコードのホスト名で、弾かれていることが判明しました。
# nslookup 10.195.100.9
9.100.195.10.in-addr.arpa       name = ip-10-195-100-9.ap-northeast-1.compute.internal.

Authoritative answers can be found from:

動作確認

DNS名で許可しているサーバからのみ、Sambaマウントできることを確認します。 許可しているログサーバのDNS名は、logserverです。

  • Sambaサーバの許可設定
[root@SambaServer ~]# cat /etc/samba/smb.conf
[global]
:
dns proxy = yes
hostname lookups = yes
:
include = /etc/samba/smb.d/test.conf
[root@SambaServer ~]#
[root@SambaServer ~]#
[root@SambaServer ~]# cat /etc/samba/smb.d/test.conf 
[test]
:
  hosts allow = 127. logserver
  • 逆引きを設定していおらず、アクセス許可もされていない他のクライアントからだと、Sambaマウントできない。
[root@NotAllowedSambaClient ~]# hostname -I
10.195.100.9
[root@NotAllowedSambaClient ~]#
[root@NotAllowedSambaClient ~]#
[root@NotAllowedSambaClient ~]# nslookup 10.195.100.9
9.100.195.10.in-addr.arpa       name = ip-10-195-100-9.ap-northeast-1.compute.internal.

Authoritative answers can be found from:

[root@NotAllowedSambaClient ~]#
[root@NotAllowedSambaClient ~]#
[root@NotAllowedSambaClient ~]# mount.cifs //${Sambaサーバ}/test test -o vers=3.0,password=,dir_mode=0755 -vvv
mount.cifs kernel mount options: ip=${Sambaサーバ}/,unc=\\${Sambaサーバ}/\test,vers=3.0,dir_mode=0755,user=root,pass=********
mount error(13): Permission denied
Refer to the mount.cifs(8) manual page (e.g. man mount.cifs)
  • 逆引きレコード設定済みのアクセス許可のあるログサーバ(logserver)からだと、Sambaマウントできる。
[root@logserver ~]# hostname -I
10.195.116.243
[root@logserver ~]#
[root@logserver ~]#
[root@logserver ~]#  nslookup 10.195.116.243
243.116.195.10.in-addr.arpa     name = logserver.honyarara.com.

Authoritative answers can be found from:
[root@logserver ~]#
[root@logserver ~]#
[root@logserver ~]# mount.cifs //${Sambaサーバ}/test test -o vers=3.0,password=,dir_mode=0755 -vvv
mount.cifs kernel mount options: ip=${Sambaサーバ},unc=\\${Sambaサーバ}\test,vers=3.0,dir_mode=0755,user=root,pass=********
[root@logserver ~]#
[root@logserver ~]#
[root@logserver ~]# df -h
ファイルシス           サイズ  使用  残り 使用% マウント位置
:
//10.195.101.57/test    50G   19G   32G   38% /root/test

感想

今回は、古のシステムを触ったので供養ブログとなりましたが。
事例がパッと見つからない課題は、毎回取り組むのが楽しいです。
困った時は、ドキュメントを読むのが大事ですね。

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

【入社エントリ】エンジニアとしてエニグモに新卒入社しました!

こんにちは! 株式会社エニグモに22年新卒入社しました橋野です。
サービスエンジニアリング本部でBUYMAのサービス開発を担当しています。

今回の記事では、エニグモへの入社理由についてお話ししようと思います。

目次

わたしとは?

まずは簡単に私について自己紹介できればと思います。 学生時代はハッカソンが好きで、たくさんハッカソンに出場したり、また私自身もプログラミングの団体に所属しておりハッカソンを主催したりしたこともありました。

ハッカソンにたくさん出場するようになったきっかけは、人数合わせで初めて出たハッカソンで悔しい思いをしたので、なんとかリベンジをしたいと思ったからです。たくさん出るうちに楽しくなっていき、どんどんのめりこみました。

テックイベントに参加するのも好きで、コロナ禍の前はよく学校終わりに勉強会やLT会などにも積極的に参加していました。 エニグモでも勉強会がほぼ毎週開催されているので、参加しています。

実は、元々小さい頃から機械が好きで、小学生の頃からロボットをいつか作ってみたいなと思っていました。大学3年生のときに、マイコンを購入し、おもちゃを趣味でつくったこともあります。そういうところも、ハッカソンやプログラミングに興味を持つきっかけになったかもしれないです。

コロナ禍のエンジニア就活

大学3年生にあがったくらいで就活を意識し始めました。 新卒エージェントサービスを使ったり、Wantedlyや逆求人イベント等のエンジニア向けのサービスを使ったりしてたくさんの企業を見ました。

勤務拠点が東京の会社を希望していたので、オンラインメインの就活は関西に住んでいた私にとって長距離の移動が少なく活動しやすい環境だったと思います。

ただ、自宅から面接を受けていたので、面接中は家族に協力してもらったりと気を遣わすこともあり、家族は大変だと思います。 また、友人にESを添削してもらったり、就活の相談に乗ってもらったりしました。

家族や友人の協力があってこその就職活動だったと思います。

エンジニア就活で色々と焦りや不安を抱えることはあったのですが、妥協せずに最後の最後まで就活をしました!

内定承諾を決めた3つの理由

選考を受けるまで、BUYMAで1回買い物をした程度でエニグモBUYMAを運営していることは知りませんでした。

エニグモとの出会いは就活をしている時に、たまたまエニグモで働いているエンジニアをフォローしており、中途採用募集のツイートを見かけました。 また、今までのツイートからエニグモのリアルを事前に知ることができ安心感がありました!

選考のスピードは他社と比べて圧倒的に早く、そこもプラスの点でした。

私が最終的に、エニグモへ内定承諾を決めた理由は下記の3つとなります。

エニグモのサービス

ECサービスが好きなので、サービス開発からユーザーの売り買いに携われたらいいなとぼんやり考えていました。 ネットショッピングは見ているだけで楽しく、ワクワクするところやサービス自体がユーザーに商品を買わせようと工夫しているところが面白いです。

わたしはより快適に買い物ができたり、新しい出会いがうまれる機会を提供できるサービスの開発をできるようになりたいと就活のサービス選びの軸として1つ持っておりました

そんな時に、見かけたBUYMAの開発ができる求人に迷わずにここだ!と思い応募しました。

Twitterで見かけた求人は中途採用のものだったのですが、中途しかなくてもとりあえず応募しようという覚悟でした。 そんな思いとは裏腹にエニグモは新卒も通年採用をやっていたので、どの時期に選考を受けても歓迎してくれました。

世界を変える新しい流れを

わたしは、大学生3年生の頃に「新しい価値」を提供できるようになりたいと思いました。学生の頃は学生なりに取り組んでいましたが、社会人になっても会社を通してもっと大きなことに挑戦したいと考えていました。
エニグモには、「世界を変える新しい流れを」というミッションをかかげており、
そのミッションに共感し、エニグモで新しい価値を提供していきたいと思っています。
「世界を買える」と「世界を変える」をかけているらしく、おもしろいですよね。

エニグモで働く人

エニグモで働いている人は、やさしい人が多いです!
面接や面談でも私自身に興味を持ってくださり、知ってくれようとしたことが印象的でした。また、自分を着飾るのではなく、正直に話したときに受け止めてくださったことが嬉しかったです。

入社後も部署関係なく交流ができたり、登山が好きな人がいたり、楽しい人が多いです。
(登山は、誘っていただきましたが運動不足ということもあり少しハードルが高くてまだ参加できていませんが...)

ファッションECというだけあって、スタイルハウス編集部やBUYMAのMDの方々はとてもおしゃれな方が多いなと思いました。(エンジニアはおしゃれやファションに興味がそこまである人ばかりではないので安心してください。)

おわりに

現在はOJTを受けながらBUYMAの開発をしています。自分自身早く現場に入って開発をしたいと思っていたので、楽しく、刺激のある毎日を送っています。
まだまだメンターのサポートありきで、開発しているので早くチームの力になれるようにたくさん吸収していこうと思います。

登山好きな人は多いのですが、ボルダリングが好きな人は少ないのでぜひボルダリングに興味がある方もお待ちしています!笑

エニグモに興味ある方は、下記募集チェックお願いします。
新卒採用も通年採用で募集しております。

株式会社エニグモ すべての求人一覧
https://hrmos.co/pages/enigmo/jobs

ITとは無縁な学生だった新卒エンジニアが振り返るエンジニア就活

はじめまして!
2022年4月にエニグモへ新卒入社した川本です。早いもので入社して2か月がたちました。

この記事では、コロナ渦での就活についてや、なぜエンジニアを目指したのか、そしてなぜエニグモに入社を決めたのかについて書いていこうと思います。

新卒でエンジニアを目指している方、エニグモに興味を持っている方へ、少しでも参考になれば幸いです。

目次

自己紹介/エンジニアを目指したきっかけ

2021年12月からインターンとしてエニグモで働いており、2022年4月に新卒入社しました。

サービスエンジニアリング本部に所属し、海外通販サイト『BUYMA』のWebアプリケーション開発に携わっています。

大学時代はアマチュアキックボクサーとして活動していたのですが、骨折して動けない期間に暇になり、家の中でもできる何か新しいことを始めようと思い、はじめて自分でパソコンを買いました。

それまでは、実はマイパソコンすら持っておらず、大学のパソコンルームでレポートなどを書いていました...

最初はこれから研究室の配属もあるし、パソコンスキルを身につけようかといった考えで購入しました。

大学の数値計算の講義で、Fortranというプログラミング言語に触れたことや、大学院に新設されたAIのコースを知って、プログラミングやITに少し興味を持ったことをきっかけに、ドットインストールなどのオンライン学習サービスでプログラミングの勉強をするようになりました。

とはいっても、私は大学で海洋学を専攻していてITとは無縁な所にいたので、この時はまだエンジニアになろうとは全く考えていませんでした。

その後も個人で学習は継続して、大学3年の夏に初めてWEB系企業のインターンで開催していたハッカソンに参加しました。このハッカソンはランダムに組まれた3人チームで行い、自社のAPIを使ってWEBアプリケーションを作るといった内容でした。

初めてのハッカソンの参加でしたが、チームメンバーにも恵まれて最優秀賞をいただくことができました。

この時初めて自分達で考えたアイデアをサービスにして、そのサービスを使ってもらいフィードバックをもらうという経験をしたのですが、この体験にとてもやりがいを感じることができ、エンジニアとして仕事がしたいと思うようになりました。

就職活動について

コロナ禍での就活で大変だったことや気を付けたこと

この記事を読んでいただいている、就活生の皆さんにとっては既にリモート就活が一般的かもしれませんが、私の感じたリモート就活のメリット、デメリット、気をつけたほうがいいことを紹介できればと思います。

メリット

  • 企業に足を運ぶ必要がないため、一日に複数社の選考を受けることができます。
  • 交通費の節約になる。特に地方の学生は、IT系などの東京に集中している企業は受けやすくなったと思います。
  • 大学と就活の両立がしやすい。大学にいながら研究の合間に面接を受けることができるのはとても助かりました。

デメリット

  • 会社や面接官の雰囲気がわかりにくい。
  • 自分の意見が相手に伝わっているかどうかわかりにくい。

気をつけたこと

デメリットに挙げた通り、リモートだと会社や面接官の雰囲気がわかりにくいことがあると思います。

最初のカジュアル面談や一次面接はリモートでいろんな企業を受けることに非常にメリットを感じましたが、選考が進めばどこかのタイミングで対面での面接の機会を設けることをおすすめします。

実際にオフィスを見学したり、働いている社員の方々を見ることで感じることも多いと思います。

私は内定承諾する際は、内定先の企業に実際に訪れるようにしていました。その中でエニグモの雰囲気が一番自分にあっていると感じて内定承諾しました。

エニグモの採用(説明会・面接・面談)への印象

就活を始めた初期にエニグモの求人をたまたま見つけて、そこで初めてBUYMAを運営している企業なのだと知りました。BUYMAのことは以前から知っていて、使ったこともあったので、まずはお話を聞いてみようといった感覚で応募しました。

エントリー後は、人事面談、一次面接、二次面接、最終面接といった選考フローで進んでいきました。

エニグモの採用で驚いたことは、一次面接からエンジニア部門の部長やマネージャーの方に面接していただけることです。 他社では一次面接は人事、二次面接では現場のエンジニアといったケースが多かったため、珍しいなと感じました。

内定後も新卒でエニグモに入社したエンジニアの方との座談会の機会を設けていただきました。同年代の方にざっくばらんに質問することができて、自分の中で最終確認することができました。

エニグモに入社した理由

私がエニグモへ入社を決めた理由は、大きく三つあります。

一つ目は、CtoCのサービスを扱っていることです。私は大学で海洋学を専攻していたため一次産業が身近で、生産者と消費者を繋ぐプラットフォームについて興味がありました。BUYMAはファッションをメインに扱っていて一次産業とは異なりますが、ユーザーとユーザーを繋ぐCtoCという面でシステム的に近いプラットフォームであったため、BUYMAの開発をしてみたいと思いました。

二つ目は、自分の興味や、やりたいことを尊重してくれるところです。私は学生時代はデータ基盤の開発しかほぼやったことがなく、アプリケーションの開発はあまりやったことがありませんでしたが、アプリケーションの開発がやりたいと伝えたところ柔軟に受け入れてくださいました。

三つ目は、エニグモの社員の人たちの雰囲気がいいなと思ったからです。選考を通して、決まった質問をする感じではなく丁寧に私のことを考えて質問してくださっている印象があり、一緒に働きたいと思える方が多かったです。

入社して2ヶ月たち感じること

入社前から抱いていたイメージと大きく違うことはありませんでした。

ただ、想像していた以上にBUYMAは大きなシステムだなと思いました。

当たり前かもしれませんが、これまで私がハッカソンや個人で開発していたシステムとはソースコードの量が全然違い、理解するのが大変です。

しかし、大きなシステムである分知らないことがたくさんあるため、毎日新しい学びがあり成長を実感できるため楽しく開発ができています。

今後の抱負

まずは目の前の業務をしっかりと一人でこなせるようになりたいです。

今はまだ経験が浅いので、いろんな分野を経験して、自分の興味がどこにあるのかを見つけて、その分野に強みを持てるようになっていきたいです。

また言われた通りに開発するだけでなく、しっかりと自分の意見を持ちサービスをより良くするためにどうするべきか考えて実装できるエンジニアになりたいと思っています。

最後に

今はエンジニアが人気職業になって世の中に様々な情報があふれているため、エンジニアを目指す就活生にとっては、何が正しいのか中々判断がつきにくいと思います。

私も非情報系学部の学生だったためその一人でした。

そんな私が就活を通して大切だと感じたのは、ハッカソンインターンに参加して同じ学生のエンジニア仲間を作ったり、現役のエンジニアと交流することです。

周りの同じエンジニア志望の学生が何をしているのか、現役のエンジニアはどのようにしてエンジニアになったのかを知ることで、自ずと自分に今何が足りなくて、これから何をすればいいのかわかってくると思います。

またそういった仲間がいると技術の話や就活の話を共有できるため、楽しく学習を継続することができてモチベーションも保てると思います。

最後までお読みいただきありがとうございます。

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

hrmos.co

Amazon Auroraのポイントインタイムリカバリ(特定時点へのリストア)を触りました。

こんにちは、インフラエンジニア の 加藤(@kuromitsu_ka)です。
今回は、Amazon Auroraのポイントインタイムリカバリ(特定時点へのリストア)を触ったので、記事を残します。

概要

Auroraのバックアップ保持期間内であれば、特定の時点のデータで、DBクラスタを作成できる機能があり、これが便利でした。
DBクラスタの作成にかかる時間と、DB作成時にどこまで正確にデータを復元できるか確認したので、そのまとめを記載します。
※手順の方は、ドキュメントに記載あるので割愛します。

ざっくりよかったこと

  • バックアップ保持期間であれば、秒単位で指定した時点でDBクラスタを作成できる。
  • 運用中のDBを切り戻すのでなくDBを新規作成するため、現行DBに手を加えなくて良い。

ちょっと細かいところ。

Auroraは、フルバックアップと合わせて、トランザクションデータも保存しています。
一番早くて、最新時間の5分前の時間で、DBクラスタを作成できる模様でした。
※切り戻せる時間の範囲については、describe-db-clusters から確認できます。

参考になる公式ドキュメント

検証したこと

ざっくり2つ確認しました。

(検証1)DBクラスタ再作成にかかる時間の計測

Auroraクラスタのステータスが、作成中から使用可能になるまでの時間を確認しました。

$ cat check.sh
#!/bin/bash

while true
do
  echo $(date +"%Y%m%d %H:%M:%S";aws rds describe-db-clusters --db-cluster-identifier $1 | jq -r ".DBClusters[].Status")
  sleep 1
done
  • 結果としては、以下のようになりました。 積んでいるデータが大きいと、そこそこ時間はかかりそうなものの、まぁ大丈夫かと。
データ 所用時間 インスタンスタイプ データのサイズ
開発環境データ 15分程度 db.t3.small(vCPU:2,メモリ:2GiB) 30GB
本番環境データ 27分程度 db.r6g.4xlarge(vCPU:16,メモリ:128) 160GB
おまけの計測

Auroraのリストアも、フルバックからリストア後、差分適用してるのかな?と思い。
開発環境にて、フルバックアップ取得時間を軸に、ざっくり2パターンで検証しました。
結果として、特に大差はなかったです。

  • Auroraのフルバックアップ取得から、1時間経過した時点のデータでDB作成した場合
    →約13分30秒

  • Auroraのフルバックアップ取得より、1時間前(23時間経過した)の時点のデータでDB作成した場合
    →約14分00秒

(検証2)切り戻し時点の前後のトランザクションデータの正確性の確認

切り戻したい時間帯に発生していたトランザクションのデータは、ちゃんと復元できるのか確認しました。

検証の結果

リストアでデータが正確に復元できないのは、リストアで指定する時間の前後1秒間にコミットされていないトランザクションだけでした。

具体的な検証方法

スクリプトでINSERT,UPDATEを流して、トランザクションを発生させました。
スクリプト実行中の時間を指定して、DBクラスタを作成して起動後のデータを確認しました。

検証ログ

データの追加時間・更新時間がわかるように、こんな感じのテーブルを作成してトランザクションを発生させました。

+------------+-------------+------+-----+-------------------+-----------------------------+
| Field      | Type        | Null | Key | Default           | Extra                       |
+------------+-------------+------+-----+-------------------+-----------------------------+
| id         | int(11)     | NO   | PRI | NULL              |                             |
| comment    | varchar(20) | YES  |     | NULL              |                             |
| created_at | timestamp   | NO   |     | CURRENT_TIMESTAMP |                             |
| updated_at | timestamp   | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+------------+-------------+------+-----+-------------------+-----------------------------+

外部のサーバから、任意の数のトランザクションを発生させるスクリプトです。
※DELETEも流していた時の名残もありますが、こちらはスルーでお願いします。

for i in $(seq ${NUMBER_OF_QUERIES_INSERT_and_UPDATE})
  mysql -h $RDS -u${user} -p${password} -e "INSERT INTO ${TABLE_NAME}  (id, comment) VALUES (${i},\"insert${i}\");"
  mysql -h $RDS -u${user} -p${password} -e "UPDATE ${TABLE_NAME} set comment=\"update${i}\" where id=${i};"
#  if [ ${i} -lt ${NUMBER_OF_QUERIES_DELETE} ] || [ ${i} -eq ${NUMBER_OF_QUERIES_DELETE} ]; then
#    mysql -h $RDS -u${user} -p${password} -e "DELETE FROM ${TABLE_NAME} WHERE id = ${i};"
#  fi
done
echo $(date "+%H:%M:%S") Script END >> ${STATUS_FILE}
  • スクリプトの出力するステータスファイルから開始終了時間を確認
# cat /tmp/status.txt 
2022-01-31 11:58:21 Script START
2022-01-31 21:52:14 Script END

f:id:enigmo7:20220304153959p:plain

  • リストア後のテーブル
    2021/01/31 21:51:59秒までのデータは、問題なく切り戻せていました。
mysql> select * from  bm_messages.AWSNEXT_1215 ORDER BY id desc limit 5;
+--------+--------------+---------------------+---------------------+
| id     | comment      | created_at          | updated_at          |
+--------+--------------+---------------------+---------------------+
| 379369 | insert379369 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379368 | update379368 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379367 | update379367 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379366 | update379366 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379365 | update379365 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
+--------+--------------+---------------------+---------------------+
5 rows in set (0.01 sec)

感想

DBを切り戻すんじゃなくて、別途作成してくれるところがいいなぁって思いました!

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

hrmos.co

dbt x BigQueryを使ってみた

こんにちは、エニグモでデータサイエンティストをしている堀部です。 昨年末から使い始めたdbt x BigQueryについて共有します。 BigQuery歴2年、SQL歴5年ほどになります。 QUALIFY句が好きです。

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をエディターで書いていた時より効率よくクエリを作成することができとても便利に感じています。今回紹介しきれなかったtestdocsなども業務では活用しています。今後はBigQueryMLと組み合わせて、前処理〜モデルの学習・推論までを全てdbt x BigQueryで完結させられたらなと考えています。*6


株式会社エニグモ 正社員の求人一覧

hrmos.co

*1:めちゃくちゃ多段のwith句を使っていることが多いです。

*2:dbtの環境構築方法は丁寧に紹介くださっている記事がたくさんあるので割愛します。

*3:ここも汎用的な処理にしたかったのですが手を抜きました。

*4:packageの導入方法については割愛します。

*5:BigQueryのカラム名として使えない文字列が入っているとエラーになるので要注意です。

*6:dbt-mlというBigQueryMLを実行するpackageがあります。

BUYMAサービスを運営するエニグモ/VPoEインタビュー「メンバーがやりたいことを後押しするのが自分の仕事」

f:id:enigmo7:20211223152715p:plain

こんにちは、人事総務グループの廣島です。エニグモで中途・新卒採用、採用広報などを担当しています。

エニグモは「世界を変える、新しい流れを。」をミッションに、世界166ヶ国に900万人以上の会員を擁するソーシャルショッピングサイト「BUYMA」を運営しています。

今回は、エンジニア部署の部長小澤さんのインタビューをお届けします。 エンジニア組織や開発体制、エニグモのカルチャーなどについて伺いました。

※この記事は Enigmo Advent Calendar 2021 の25日目の記事です。 あっというまでアドベントカレンダーも最終日です!


 

目次


 

まずは簡単に経歴や自己紹介をお願いします

前職は新卒で入社したSIerで勤怠管理や人材管理などのパッケージシステムの開発をしていました。会社の中でも比較的開発を担当できる部署でしたが、役職が上がると開発から離れ管理がメイン(電話片手にエクセルとにらめっこみたいな)となる為、開発に関わりつづけたい、Webに行きたいと思い転職を決意し、ご縁がありエニグモに入社しました。 入社してかれこれ10年が経ち、現在エンジニアの部長を務めています。

f:id:enigmo7:20211224124143p:plain

エンジニア組織や開発体制について

現在のエンジニアの開発組織について教えて下さい

現在、エンジニアの組織は、業務委託として参画してくださっている方も含め約50名の組織となっており、4つのグループ(インフラグループ、データテクノロジーグループ、アプリケーション開発グループ、グローバルグループ)に分かれています。

小澤さんが入社してから今までで組織はどのように変わりましたか?

入社した当時、エンジニア組織としては8人程でしたので、それから比べると組織はだいぶ大きくなりましたね。私が部長になってからは、社員15人くらいまでは部長以下の役職を設けず全員フラットな組織でしたが、組織の拡大とともに、各グループにマネージャーの役職を置き、現在の組織体制となりました。

データテクノロジーグループはここ数年で新しくできた組織ですが、グループ立ち上げの経緯を教えて下さい

データテクノロジーグループは元々アプリケーション開発グループの一部で、性能改善やバックエンドの安定化などを担うチームでした。全社的にデータドリブンな環境が加速していく中で、データ収集や機械学習、検索性向上などのデータ領域に対して専門性の高いチームへ進化していき、今のデータテクノロジーグループへとなりました。 これらの進化は会社側からのトップダウンの方針ではなく、現場のマネージャーからのボトムアップで組織が進化していった経緯があります。

開発体制の特徴は?

現在、大きく3つの機能(購入者向け機能、出品者向け機能、サービスインフラ)別チームに分かれて開発案件に対応しています。各機能別にエンジニア、デザイナー、ディレクター、データアナリスト、ビジネスサイド(CS・MD等)が組織横断でプロジェクトにアサインされ開発を進める、機能別の開発体制になっています。

職種で役割を完全に分けてしまうのではなく、企画・設計段階からみんなで意見を出し合い、サービスや機能を作っていくのが特徴です。

機能別の開発体制に移行した背景やその後の変化はありましたか?

元々はプロジェクトへのメンバーのアサインは、プロジェクトの難易度や特性とメンバーの得意分野、やりたいこと、稼働等を見て判断しておりました。 しかし、BUYMAは1つのサービスとして成り立つために様々な機能が組み合わさっています。 例えば、購入者向けの機能(商品詳細ページやレコメンド、クーポン等)と出品者向け機能(出品管理・ショップ連携・お問い合わせ管理等)ではサービスの性質・要件や抱える課題は異なり、仕様は複雑になっているため、開発メンバー全員が全ての機能の特性をキャッチアップすることは難しくなっていました。

そこで、開発メンバーが機能(ドメイン)別に特化することで、専門性を持ち効果的にスピードを上げて案件に対応できるのではないかと考え、機能別開発体制となりました。

開発体制の変更によって、機能ごとにエンジニア一人一人が当事者意識を持って仕事に取り組むことができ、エンジニアもプロダクト開発の目標設定への責任感・コミット力があがったと思います。

BUYMAの開発を行う魅力は?

まずは、BUYMAというそれなりのユーザーがいるECサイト・大規模サービスに関われることでしょうか。やった事への影響も大きいですし、画面を変えれば良くなったとユーザーから褒めていただいたり、時には厳しいご指摘をいただくなど反応もありますし、トラフィックもあるのでパフォーマンスチューニングのやりがいもあります。また、ローンチして17年を超えるWebサービスなので、古いシステム・技術もあり直すことも多いので、そういうところが好きな方であればやりがいに感じる方もいるかと思います。

開発や組織の課題について

前段でもお話ししましたが、17年を超えるWebサービスの為、古いシステムやレガシーな技術もいっぱいあるところですね(新しい技術もどんどん導入していますが)。リフレッシュしないと開発速度が落ちてしまうので、常に技術のアップデートは行う必要があります。 技術のアップデートをするにも、機能やデータベース等が複雑に絡み合っている為、1つの技術を変えると他の機能への影響範囲も大きいため、それぞれチューニングが必要です。そのあたり開発・運用サイクルを効率よく回すためにも、いくつかの改善案を検討しています。

上記のような課題もある為、新しいサービスや機能を開発したいというエンジニアだけでなく、レガシーな技術をモダン化したい、開発が上手くまわる仕組みや環境を作りたい・整えたいというエンジニアも組織全体としてスピードアップして開発する為には必要であり、活躍の機会があります。

エンジニア採用で大切にしていることや、活躍するメンバーとは

エンジニア採用で大切にしていることはありますか?

新メンバーが入社した際にも、その人がやりたいことをやる方がきっといいと思っているので、「こういうことがやりたい」というモチベーションが高く、且つ自走力があり実行できる人がいいですね。実は、私も上からあれしろこれしろとあまり言われないので、エンジニアにもあれしろこれしろ言わないようにしています。やりたい人に任せることが一番いい結果を生むと感じています。

また、成長意欲の高い人や向学心の強い方にはオススメな環境です。 なぜなら、BUYMAはさまざまな機能が絡み合うため、システムとして非常に複雑になっています。新メンバーが、特に若手メンバーがシステムの概要をキャッチアップ・理解するまでに時間がかかり若手に与える適切な課題・タスクを切り分けるのが難しい場面もあります。 「教科書で学んできたことが教科書通りにはいかず、開発する上であれもこれも詰め込まれるので、一気に10人くらいに殴られる感覚になる」と、ある若手メンバーが言っていたのが印象的です。

1つ1つ少しずつ成長したい人にとっては、最初は我慢が必要かもしれません。その分成長のスピードは早く、BUYMAで開発ができればだいたい何でも開発できるようになると思うので、臆せずチャレンジしていただきたいですね。

エンジニアの雰囲気や活躍するメンバーの特徴は?

色々なキャラクターの人がいますが共通項としては、みんなまじめで何事も一生懸命な頑張り屋が多い印象です。また、エンジニアに関わらず、エニグモのメンバーはいい人が多いと思います。

活躍するメンバーの特徴は、アイデアがある人じゃないでしょうか。アイデアというのは、新規の提案だけなく、定例やMTG内でも自分の意見や考えを発言し、発言して終わりではなく行動が伴う方です。そういったメンバーが周囲からも信頼を得られ活躍しているように感じます。

f:id:enigmo7:20211223160246p:plain

エニグモの技術選定について

メンバーやマネージャーからのボトムアップで決まる場合が多いですね。 経営判断や、エンジニアのマネージャー会議等で、これを導入していこうというよりは、 専門性のある各メンバーが今の課題の中からこの技術が良いのでないかと判断し導入します。

もちろん例外もあります。規模感にもよりますが、さすがにBUYMA全体の言語をPHPからRubyへ変更しようとなった時や、BUYMAのインフラ環境をオンプレからAWSへ移行等の規模が大きい案件の場合は役員プレゼンし承認を得ています。そのほか、技術導入による影響範囲が大きい場合や、導入にお金が絡む際も私のところに相談が来ますね。

エンジニアのキャリアについて

どのようなキャリアアップの選択肢がありますか?

スペシャリストとしてもマネージメントとしてもキャリアアップの選択の機会があります。 役職や肩書がなくてもスペシャリストとして高い報酬が得られる給与体系となっており、本人の志向や経験スキルに応じて柔軟なキャリアステップを歩んでいただけます。

マネージャーへはどのようにアサインされるのでしょうか?

エニグモのエンジニアの特徴としては、チームを作りたいという人よりも、スペシャリストになりたい人の方が多いように感じます。その為、マネージャーを任せたいメンバーには1on1等でやってみる?と聞いています。

一概には言えませんが、任せたいと思うエンジニアはすでにメンバーの役割を超えて、チームをまとめていたり、PM・リーダーとしての動きをしているので、そこからマネージャーをやってみようかとなるパターンも多いように感じます。意図的というよりも自然とそうなっています。

フロントエンドエンジニア、サーバーサイドエンジニアを明確に分けていないのも特徴ですよね

はい。エニグモではエンジニアの担当領域をフロントエンド、バックエンドではなく、サービス・プロジェクト単位でアサインしている為、フルスタックな知識・経験やスキルをつけることが出来ます。

なぜ分けていないかというと、フロントエンド・バックエンド両方できた方がやりがいや完成した際の達成感があると考えているためです。 せっかく画面があるサービスなので裏側から表に出てくるまでの一連の流れをやった方が楽しいと思うため、やりたいメンバーにはサーバーサイド、フロントエンドとわずにプロジェクトにアサインし任せています。

部長として大切にしていることはありますか?

みんなが楽(ラク)になればよいなと思っています。楽というのは業務が上手くまわる感じですね。

1on1でも、今の仕事は楽しいか、何をやりたいのかを聞くようにしています。何が好きで何がやりたいかを聞いてそれを実現できる環境を作ることが今の私の役割だと思ってます。 経営陣もエンジニア組織のみならず、各部門の意思を最大限尊重してくれる文化なので、部長としてもやりやすい環境だと思っています。


 

以上、エンジニア部長の小澤さんのインタビューでした! こちらで、 Enigmo Advent Calendar 2021 は以上となります。今年も色々な記事がありましたね。 2022年もよろしくお願いします!


株式会社エニグモ 正社員の求人一覧

hrmos.co