株式会社エニグモのブログ - TECH PLAY

TECH PLAY

株式会社エニグモ

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

252

こんにちは、テッ クリード の 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
こんにちは、インフラエンジニア の 加藤( @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 のサービス開発を担当しています。 今回の記事では、 エニグモ への入社理由についてお話ししようと思います。 目次 わたしとは? コロナ禍のエンジニア就活 内定承諾を決めた3つの理由 エニグモのサービス 世界を変える新しい流れを エニグモで働く人 おわりに わたしとは? まずは簡単に私について自己紹介できればと思います。 学生時代は ハッカソン が好きで、たくさん ハッカソン に出場したり、また私自身もプログラミングの団体に所属しており ハッカソン を主催したりしたこともありました。 ハッカソン にたくさん出場するようになったきっかけは、人数合わせで初めて出た ハッカソン で悔しい思いをしたので、なんとかリベンジをしたいと思ったからです。たくさん出るうちに楽しくなっていき、どんどんのめりこみました。 テックイベントに参加するのも好きで、コロナ禍の前はよく学校終わりに勉強会や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
はじめまして! 2022年4月に エニグモ へ新卒入社した川本です。早いもので入社して2か月がたちました。 この記事では、コロナ渦での就活についてや、なぜエンジニアを目指したのか、そしてなぜ エニグモ に入社を決めたのかについて書いていこうと思います。 新卒でエンジニアを目指している方、 エニグモ に興味を持っている方へ、少しでも参考になれば幸いです。 目次 自己紹介/エンジニアを目指したきっかけ 就職活動について コロナ禍での就活で大変だったことや気を付けたこと エニグモの採用(説明会・面接・面談)への印象 エニグモに入社した理由 入社して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
こんにちは、インフラエンジニア の 加藤( @kuromitsu_ka )です。 今回は、 Amazon Aurora のポイントインタ イムリ カバリ(特定時点へのリストア)を触ったので、記事を残します。 概要 Auroraのバックアップ保持期間内であれば、特定の時点のデータで、DB クラスタ を作成できる機能があり、これが便利でした。 DB クラスタ の作成にかかる時間と、DB作成時にどこまで正確にデータを復元できるか確認したので、そのまとめを記載します。 ※手順の方は、ドキュメントに記載あるので割愛します。 ざっくりよかったこと バックアップ保持期間であれば、秒単位で指定した時点でDB クラスタ を作成できる。 運用中のDBを切り戻すのでなくDBを新規作成するため、現行DBに手を加えなくて良い。 ちょっと細かいところ。 Auroraは、 フルバック アップと合わせて、 トランザクション データも保存しています。 一番早くて、最新時間の5分前の時間で、DB クラスタ を作成できる模様でした。 ※切り戻せる時間の範囲については、 describe-db-clusters  から確認できます。 参考になる公式ドキュメント 特定の時点への DB clusterの復元 Aurora DB クラスターのバックアップと復元の概要 検証したこと ざっくり2つ確認しました。 DB クラスタ 再作成にかかる時間の計測 切り戻し時点の前後の トランザクション データの正確性の確認 (検証1)DB クラスタ 再作成にかかる時間の計測 Aurora クラスタ のステータスが、作成中から使用可能になるまでの時間を確認しました。 モニタリング用に、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 | +------------+-------------+------+-----+-------------------+-----------------------------+ スクリプト のINSERT,UPDATE部分 外部のサーバから、任意の数の トランザクション を発生させる スクリプト です。 ※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 DB クラスタ ー作成 スクリプト 終了間際の、2021/01/31 21:52:00に切り戻します。 リストア後のテーブル 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について共有します。 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