TECH PLAY

株式会社エニグモ

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

241

こんにちは。サーバーサイドエンジニアの伊藤です。 この記事は Enigmo Advent Calendar 2020 の 5 日目の記事です。 さっそくですが、みなさん 2 段階認証(2FA) の ワンタイムパスワード の発行には何を利用していますか? 私は普段 Authy という 2 段階認証アプリを利用しています。ただ、 AWS を コマンドライン から操作する時などわざわざターミナルからアプリに移動して ワンタイムパスワード をコピーして貼り付けるのが面倒だと思うことがありました。 ということで、 OATHTOOL と peco を利用して CLI から ワンタイムパスワード (TOTP)を取得するラッパーコマンドを作成しました。 今回は折角なので 2 段階認証(2FA) についてと 2 段階認証アプリで利用される ワンタイムパスワード (TOTP) についても調査したのでまとめました。 最後に作成したラッパーコマンドについて少しだけ紹介します。 Two-Factor Authentication(2FA) HOTP vs. TOTP HOTP: HMAC-based One-Time Password (RFC 4226) TOTP: Time-based One-Time Password (RFC 6238) OATHTOOL ラッパーコマンド Usage Demo Details 参考 Two-Factor Authentication(2FA) そもそも、Two-Factor Authentication(2FA) とは何なのか? ログインする際にユーザー名とパスワードに加えて、もう一つ追加の要素を要求する認証方法です。 万が一パスワードが漏洩した場合でも追加の要素を知らない限りログインすることができません。 これにより、セキュリティの強度を高めることが可能です。 一般的に追加の要素には下記の方法が用いられます。 Something you know(認証を行う本人のみが知る情報) e.g. 認証番号(PIN)、パスワード、秘密の質問への回答 etc... Something you have(認証を行う本人が所有するもの) e.g. クレジットカード、 スマートフォン 、OTP ジェネレーター etc... Something you are(認証を行う本人の身体的特徴) e.g. 指紋認証 、網膜認証、声紋認証 etc... みなさんご存知の Authy や Google Authenticator といったアプリはこの Something you have を用いた認証方法の一種です。 これらのアプリでは 2 つめの要素である Time-based One-Time Password(TOTP) を生成・取得することが可能です。 HOTP vs. TOTP ここででてきた Time-based One-Time Password(TOTP) とは何なのでしょうか? TOTP についてそのもととなる HMAC-based One-Time Password(HOTP) と合わせて説明していきます。 HOTP: HMAC-based One-Time Password ( RFC 4226 ) 8-byte のカウンター(可変値)と 秘密鍵 をもとに ワンタイムパスワード を生成します。 これらの情報をクライアント側とサーバー側で共有することで認証を行います。 HOTP を生成するための アルゴリズム は下記のようになっております。 詳細は省きますが可変値であるカウンターと 秘密鍵 を HMAC に渡してその返り値を truncate することで ワンタイムパスワード を生成します。 // Algorithm C 8-byte counter value, the moving factor. This counter MUST be synchronized between the HOTP generator (client) and the HOTP validator (server). K shared secret between client and server; each HOTP generator has a different and unique secret K. HOTP(K,C) = Truncate(HMAC-SHA-1(K,C)) 出典: HOTP: An HMAC-Based One-Time Password Algorithm TOTP: Time-based One-Time Password ( RFC 6238 ) 次は TOTP です。 下記の アルゴリズム からわかるように、TOTP の アルゴリズム の根本的部分は HOTP のものと同様です。唯一可変値であるカウンターの代わりに時間表現が用いられることが HOTP とは異なります。 時間表現と 秘密鍵 をクライアント側とサーバー側で共有しこれらをもとに発行した ワンタイムパスワード を利用し認証を行います。 // Algorithm X represents the time step in seconds (default value X = 30 seconds) and is a system parameter. T0 is the Unix time to start counting time steps (default value is 0, i.e., the Unix epoch) and is also a system parameter. T = (Current Unix time - T0) / X TOTP = HOTP(K, T) 出典: TOTP: Time-Based One-Time Password Algorithm 中身を見てみると HOTP も TOTP も想像よりはるかにシンプルだということがわかります。 アルゴリズム 自体も簡単なものなので自前で実装してみるのも面白そうですね。 OATHTOOL ラッパーコマンド さて、最後に少しだけ今回作成した CLI から ワンタイムパスワード (TOTP)を取得する OATHTOOL のラッパーコマンドを紹介します。 Usage $ mfa -h usage: mfa [-h | --help ] [ - [ no ]-c | --[ no ] -copy ] [-a < account >| --account < account >] [-l | --list ] -v , --version Prints the version. -h , --help Prints this message. - [ no ] -c, --[no]-copy Copies the generated token to the Clipboard. ( default ) -a < account > , --account < account > Copies the generated token of < account > to the Clipboard. -l , --list Prints a list of available authenticator accounts. 基本的には peco を利用し、存在するアカウントから TOTP を取得したいアカウントを選択します。 -a|--account を利用することで TOTP を取得したいアカウントを明示的に指定することも可能です。 Demo Details ラッパーコマンドの利用方法は下記をご覧ください。 github.com 最後まで読んでいただきありがとうございました。 明日の記事の担当はサーバーサイドエンジニアの橋本さんです。お楽しみに。 参考 What Is Two-Factor Authentication (2FA)? (最終アクセス: 2020/11/14) HOTP: An HMAC-Based One-Time Password Algorithm (最終アクセス: 2020/11/14) TOTP: Time-Based One-Time Password Algorithm (最終アクセス: 2020/11/14) 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、サーバーサイドエンジニアの寺田です。 この記事は Enigmo Advent Calendar 2020 の 4日目の記事です。 みなさんはエンジニアへの転職!と聞くとどんなことをイメージするでしょうか??? もちろん スキルアップ できそうといったポジティブなイメージもあると思いますが、 沸沸とこんな心の声が聞こえてきそうです。。。 自分のプログラミングのスキルでやっていけるのだろうか。。。 まともな導入もなくいきなり業務にポーン!あとはよろしく!ってされそう スキルアップ できると思ったのに任されるのは簡単な作業ばかり。。。 新しい環境で既存社員の方と仲良くなれるかな。。。 私も!と思うことが1つでもあればぜひこの記事を見ていってください! エニグモ のオンボーディング *1 を知って頂いて、 エニグモ で働いてみたい!と思っていただける方がいてくれると幸いです。 自己紹介 かくいう私も エニグモ に入社したのは2020年の7月で、本記事の執筆時点でちょうど歴半年になります。 新卒で入社した SIer を退職して、 エニグモ が2社目になります。 前職は SIer と言ってもプログラミングは全くしないポジションで、顧客折衝や開発担当のマネジメントが中心でした。なのでエンジニアとしては業務未経験と言える状態でした( ;∀;) 加えて私が入社したのは コロナウイルス による感染予防真っ只中の7月、 エニグモ も例に漏れず原則フルリモートでの勤務でした。 なかなか人と直接会うこともできない状況下で現メンバーと円滑にコミュニケーションをとって仕事進められるのか?不安でいっぱいでした。 そんな中大きな支えとなったのが エニグモ のオンボーディングです。 本記事では実際に私が経験したことについて具体的に説明していきます! 完全サポート!メンター制度 エニグモ では新人エンジニアには先輩社員をメンターとしてつけてもらえます! メンターはこんなことの面倒を見てくれます。 毎日朝会を開催してタスクの進捗状況をウォッチしてくれる。 また困り事があれば質問タイムを設けてくれるのでたちまち解決できます。 毎週振り返りをして今週のタスクや勉強から学んだことを確認。今の自分に足りていないスキルを分析して、次にどんな勉強をしたらいいか?どんなタスクに アサイ ンして欲しいか?など相談できます。 技術的な相談。設計で悩んだところや、コードの不明点、エラーハンドリングなどなんでもOK(Slackに投げると秒で教えてくれます(つД`)ノ) このメンターについてもらえるというのは精神的な部分でやっぱり安心できます!新しい職場で誰に相談したらいいかもわからない。。。というのはありがちですが、この制度のおかげで全く問題なかったです!また エニグモ は現在フルリモート勤務ですが、少なくともメンターと毎日コミュニケーションを取れるので、寂しさを感じることもありません。 成長できる環境 エニグモ には業務以外でもスキルを伸ばせる環境があります。 その中でも本記事では以下の3つについて紹介させていただければと思います。 勉強会 モブプログラミング オリジナル アプリ開発 勉強会 若手の社員を中心に週1回の勉強会が開催されています。 形式は 輪講 という形で、順番に勉強した内容をスライドにまとめて発表し、みんなが質問したり議論したりわいわいと勉強しています! ここでは私が半年間、実際に勉強会で読んできた本を紹介します。 gihyo.jp オブジェクト指向設計 の名著と呼ばれる1冊ですね! 理解が難しい オブジェクト指向 の概念ですが、豊富な実装例が示されており初学者でも理解しやすい点が魅力です。 また オブジェクト指向 本はサンプルコードが Java なことが多いですが、この本は Ruby で書かれていることもポイントです。 www.oreilly.co.jp Ruby において強力な武器である メタプログラミング を学べる1冊です! メタプログラミング そのものを学べることも魅力ですが、 その仕組みに触れることで Ruby という言語のコアな部分を学ぶことができ実は初学者にとってもとても有益な本だと思います。 題材とする本は毎回メンバーみんなで決めています。 自由に学びたいことを学べるそんな勉強会になっています! モブプログラミング 若手とベテラン社員数人でzoomをつないでモブプログラミングをしています。 モブプログラミングとは、ドライバーと呼ばれる一人がコーディングしている画面をみながら、そこにいるメンバーが意見や、提案しながら進めるプログラミング手法の一つです。もう少し詳しく知りたい方は以下の記事などは参考になるかと思います! モブプログラミングスタートアップマニュアルを更新しました! | TAKAKING22.com モブプログラミングのメリットはいろいろあると言われていますが、 新人観点で見ると何よりベテランの手元を覗けること、それが一番のいいところだと思っています。コーディングしている時の思考方法とか、ベテランのスピード感を目の前で感じることができます。またエンジニアはそれぞれ自分の生産性を上げるために開発環境にこだわっていろいろカスタマイズしています。ですがそれを教えてもらえる機会というのはなかなかないんですよね。モブプログラミングでベテランの画面を見ている時、すごっ!!何それ!!って思う機能は見て盗んで自分のものにしちゃうこともできます。 オリジナル アプリ開発 あと非常にありがたいのが、 スキルアップ のために自由にテーマを決めて開発させてくれることです! レビューもあり、フィードバックを受けられますので自己研鑽の何倍も成長できます。別に業務に関係のあるテーマでなくてもいいです。ちなみに私は競馬予想アプリの開発をさせてもらっています(笑) 開発している競馬予想アプリ 未経験エンジニアとして入社すると、入社してしばらくの業務は簡単な作業から。。。というのは仕方ない部分でもありますが、それだけだとスキル面で中々成長できないというジレンマがあります。ただこのように自分でテーマを決めて0からアプリを開発することで、設計などのちょっと高い次元のスキルを習得できるのではないかと思います。 いかがでしたでしょうか? エニグモ には現在100名ほどの社員がおりますが、 そのような規模感の会社だと入社時のオンボーディングの取り組みが不十分なのでは?と不安になることもあるかと思います。(実際私もそうでした)しかしいざ入社してみると全然そんなことはなく、安心して業務に取り組むことができています! この記事を見て エニグモ に興味を持ってもらえるエンジニアの方が1人でもいてくだされば幸いです! 明日の記事の担当はサーバーサイドエンジニアの伊藤さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co *1 : オンボーディングとは、組織に新たに加入した人に手ほどきや支援を行い、定着を促したり慣れてもらう活動のこと。新人研修やそのあと配属後に受けられる支援プログラムなどのこと。
アバター
こんにちは、サーバーサイドエンジニアの竹本です。 この記事は Enigmo Advent Calendar 2020 の3日目の記事です。 みなさまは2020年に買った中でよかったものはなんでしょう? 私は iPad です。 最新 Apple iPad Pro (12.9インチ, Wi-Fi, 128GB) - シルバー (第4世代) 発売日: 2020/03/25 メディア: Personal Computers 主に kindle を見開きで読むことに活用しています。 エニグモ の福利厚生の一つ「エンジニアサポート」で5万円の補助を受けました。わーい。 https://enigmo.co.jp/recruit/culture/ そしてみなさまは馬券、買っていますか? 馬券は競馬に賭ける際に購入する投票券です。 1口100円から、ネットでも気軽に購入することができます。(競馬は20歳から) 弊社にも数名競馬好きが在籍しており、時折競馬 トーク で盛り上がることもあります。 「競馬必勝本」は本当に当たるのか? 「競馬必勝本」というものが巷にはあります。 こういった本では勝った時の馬券と払戻金だけセンセーショナルに紹介されていることが多く、 実際どのくらい賭けてどのくらい当たっているのかわからないことが多いです。 なので実際に本の通りに馬券を購入していたらどのくらい勝つのかを検証してみます。 今回参考にするのは「競馬力を上げる馬券 統計学 の教科書」です。 競馬力を上げる馬券統計学の教科書 作者: 大谷清文 発売日: 2019/10/31 メディア: Kindle 版 世にある競馬必勝本の中でも、オッズのデータから勝ち馬を選ぶという、タイトルの通り統計的な要素の多い本になっています。 また馬やジョッキーに関係なくオッズのみを参考にしているので、さまざまなレースがあるなかで汎用的に活用できるという利点があります。 この本に書いてあることをざっくりまとめると以下のようになります。 万馬券 を狙え 的中率ではなく回収率にこだわれ 3000円分の馬券を書い続けて3回に1回10000円が当たれば1000円プラスということですね。つまり当たれば 万馬券 になるようなレース、「穴馬」が勝ち馬にいる馬券を買う必要があります。 「 馬連 オッズの壁」の法則で穴馬を選ぼう 本書の中で最もシンプルな穴馬選択方法として紹介されているのが「 馬連 オッズの壁」の法則です。 ( 馬連 とは上位2頭を当てる馬券のこと) この法則を簡単に紹介すると レース 14頭以上出走する 馬連 1位人気オッズが9倍以上 単勝 オッズ30倍以内の馬が10頭以上 馬連 オッズ1位人気馬に 単勝 1位人気馬が含まれる 穴馬 単勝 1位人気馬の 馬連 オッズを人気順に並べた時1.8倍以上の壁がある時、その壁の前2頭を選ぶ 馬券の組み立て方 単勝 1位人気馬の 馬連 オッズ人気順の「1~4位」 - 「5~8位」 - 「穴馬」 のフォーメーション三連複 (三連複: 上位3頭を順不同に選ぶ馬券) (フォーメーションとは https://www.jra.go.jp/kouza/yougo/w528.html ) 本書ではさまざまな条件を組み合わせて馬券を選択していますが、 今回は実装の簡便さのため条件も簡略化していることをご了承ください。 実際のコード 今回は Google Colabratoryを利用しました。 Google アカウントがあれば環境構築もなしに python が使えて便利ですね。 https://colab.research.google.com/ 2020/11/27現在動くことを確認しているのでみなさまもぜひ使ってみてください。 まず必要なライブラリのインストールします !apt-get update !apt install chromium-chromedriver !cp /usr/lib/chromium-browser/chromedriver /usr/ bin !pip install selenium !pip install lxml importします import pandas as pd from bs4 import BeautifulSoup import urllib.request as req from selenium import webdriver from selenium.webdriver.chrome.options import Options import numpy as np import urllib.parse 今回用意した関数 def set_selenium (): options = webdriver.ChromeOptions() options.add_argument( '--headless' ) options.add_argument( '--no-sandbox' ) options.add_argument( '--disable-dev-shm-usage' ) driver = webdriver.Chrome( 'chromedriver' ,options=options) driver.implicitly_wait( 15 ) return driver def get_raceids (date): url = "https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=" + date res = req.urlopen(url) racesoup = BeautifulSoup(res, "html.parser" ) racelist = racesoup.select( "#RaceTopRace > div > dl > dd > ul > li > a:nth-of-type(1)" ) raceids = [ urllib.parse.parse_qs(urllib.parse.urlparse(race.get( 'href' )).query)[ 'race_id' ][ 0 ] for race in racelist ] return raceids def get_tansho_ichiban (raceid): driver = set_selenium() driver.get( "https://race.netkeiba.com/odds/index.html?type=b1&race_id=" +raceid+ "&rf=shutuba_submenu" ) html = driver.page_source.encode( 'utf-8' ) tanhukusoup = BeautifulSoup(html, "html.parser" ) tanhukudfs = pd.read_html( str (tanhukusoup.html)) tansho_ichiban = tanhukudfs[ 0 ][tanhukudfs[ 0 ][ 'オッズ' ] == tanhukudfs[ 0 ][ 'オッズ' ].min()][ '馬番' ].values[ 0 ] return tansho_ichiban, tanhukudfs[ 0 ] def get_umarenodds (raceid): driver = set_selenium() driver.get( "https://race.netkeiba.com/odds/index.html?type=b4&race_id=" +raceid+ "&housiki=c0&rf=shutuba_submenu" ) html = driver.page_source.encode( 'utf-8' ) soup = BeautifulSoup(html, "html.parser" ) dfs = pd.read_html( str (soup.html)) umarendf = pd.DataFrame(index=[ 1 ]) for i, df in enumerate (dfs): umarendf = pd.concat([umarendf, df.set_index( str (i+ 1 )).dropna(how= 'all' , axis= 1 )], axis= 1 ) if umarendf.isin([ '取消' ]).values.any() | umarendf.isin([ '除外' ]).values.any(): return False umarendf[umarendf.index.max()]= 0 umarenodds = pd.DataFrame(umarendf.fillna( 0 ).astype( 'float64' ).values + umarendf.astype( 'float64' ).fillna( 0 ).values.T, columns= list ( map ( int , map ( float ,umarendf.columns))), index=umarendf.index).replace( 0 ,np.nan) return umarenodds def get_baken (raceid): tansho_ichiban, tanhukudf = get_tansho_ichiban(raceid) umarenodds = get_umarenodds(raceid) if umarenodds is False : return False umarenninki = umarenodds.min() umaren_ichiban = umarenninki[umarenninki == umarenninki.min()].index if umarenninki.min() >= 9 and any (umaren_ichiban == tansho_ichiban) and umarenninki.index.max() >= 14 and sum (tanhukudf[ "オッズ" ]<= 30 )>= 10 : ninkiuma = umarenodds[tansho_ichiban].sort_values() anaumalist = [] for idx in np.where((ninkiuma/ninkiuma.shift( 1 ) > 1.8 ).values == True )[ 0 ]: two = idx - 1 anaumalist = anaumalist + (ninkiuma/ninkiuma.shift( 1 ) > 1.8 ).index.values[two:two+ 2 ].tolist() if not anaumalist: return False formation1 = ninkiuma.fillna( 0 ).sort_values()[ 0 : 4 ].index.values formation2 = ninkiuma.fillna( 0 ).sort_values()[ 4 : 8 ].index.values return { 'anauma' :anaumalist, 'formation1' :formation1, 'formation2' :formation2} return False def get_dayresult (date): kakekin = 0 modorikin = 0 raceids = get_raceids(date) for raceid in raceids: baken = get_baken(raceid) if not baken: continue result = pd.read_html( "https://race.netkeiba.com/race/result.html?race_id=" +raceid) sanrenpuku = list ( map ( int ,result[ 2 ].set_index( 0 )[ 1 ][ '3連複' ].split())) money = int (result[ 2 ].set_index( 0 )[ 2 ][ '3連複' ].replace( ',' , '' ).replace( '円' , '' )) if bool ( set (sanrenpuku) & set (baken[ 'formation1' ])) & bool ( set (sanrenpuku) & set (baken[ 'formation2' ])) & bool ( set (sanrenpuku) & set (baken[ 'anauma' ])): kakekin += 100 * len (baken[ 'formation1' ])* len (baken[ 'formation2' ])* len (baken[ 'anauma' ]) modorikin += money else : kakekin += 100 * len (baken[ 'formation1' ])* len (baken[ 'formation2' ])* len (baken[ 'anauma' ]) cols = [ "賭け金" , "払戻金" ] return pd.Series([kakekin,modorikin],index=cols,name=date) netkeibaから Selenium + BeautifulSoupでオッズのデータを スクレイピング します。 その結果をpandasで前処理し、上記の 「 馬連 オッズの壁」の法則 から馬券を選択 選択した馬券が当たっているのかを検証します。 回収率にこだわるのが本書の方針なので、検証項目として 条件に合致した全ての三連複馬券を100円で購入 = 賭け金 当たったレースの実際の払戻金 以上の差額を見ることにします。 今回は11月の1~23日までのレースを検証します。 # 開催日のリスト datelist = [ '20201101' , '20201107' , '20201108' , '20201114' , '20201115' , '20201121' , '20201122' , '20201123' ] moukaridf = pd.DataFrame() for date in datelist: onedaydf = get_dayresult(date) moukaridf = moukaridf.append(onedaydf) 時間かかりますが待ちましょう 結果 moukaridf.sum()[ '払戻金' ] - moukaridf.sum()[ '賭け金' ] # 出力 - 14630.0 金額としては14630円負けてしまいました。 moukaridf 払戻金 賭け金 20201101 33500.0 12800.0 20201107 0.0 9600.0 20201108 5560.0 16000.0 20201114 0.0 25600.0 20201115 45510.0 25600.0 20201121 0.0 0.0 20201122 0.0 9600.0 20201123 0.0 0.0 日別に見ると勝っている日もありますね。 本書では回収率を上げるための馬券選択方法がさらに細かく紹介されていたので、その通りに実装すればもっと良い結果となるかもしれません。 t検定をすると、「賭け金、払戻金の差額の平均が0ではない(正負どちらかに傾く)」という 帰無仮説 が棄却されます。(p > 0.05 平均 -1828.8 ± 14776 円) sagaku = moukaridf[[ '払戻金' ]].values - moukaridf[[ '賭け金' ]].values # 平均 print (sagaku.mean()) # 標準偏差 print (sagaku.std()) # 出力 - 1828.75 14776.743076114573 よって結論は「勝つこともあれば負けることもある!」 馬券を買おう ではせっかく作ったので実際に賭けてみようと思います! 検証では最終オッズから馬券を選択していましたが、当日は10:30のオッズを元に馬券を選択します。 予算の関係で条件に合致した全ての馬券ではなく、1レース選択してフォーメーション3連複馬券を購入します。 11/29の10:30時点で候補が4レースありました。 date = "20201129" raceids = get_raceids(date) for raceid in raceids: baken = get_baken(raceid) if baken: print ( 'https://race.netkeiba.com/race/shutuba.html?race_id=' +raceid) print (baken) print ( "=======================" )   # 出力 # 条件に合致したレースのURLと購入すべき馬券がプリントされる https://race.netkeiba.com/race/shutuba.html?race_id= 202005050907 { 'anauma' : [ 4 , 16 ], 'formation1' : array([ 7 , 6 , 3 , 2 ]), 'formation2' : array([ 10 , 14 , 15 , 8 ])} ============ https://race.netkeiba.com/race/shutuba.html?race_id= 202005050911 { 'anauma' : [ 11 , 6 , 1 , 16 ], 'formation1' : array([ 4 , 14 , 9 , 13 ]), 'formation2' : array([ 12 , 5 , 2 , 3 ])} ============ https://race.netkeiba.com/race/shutuba.html?race_id= 202009050904 { 'anauma' : [ 3 , 7 ], 'formation1' : array([ 8 , 15 , 5 , 1 ]), 'formation2' : array([ 16 , 4 , 13 , 12 ])} ============ https://race.netkeiba.com/race/shutuba.html?race_id= 202009050912 { 'anauma' : [ 7 , 6 ], 'formation1' : array([ 13 , 3 , 10 , 11 ]), 'formation2' : array([ 2 , 9 , 15 , 5 ])} ============ 今回は東京7Rに賭けます! (出力で一番上のレース) 穴馬が「4,16」1~4位が「7, 6, 3, 2」,5~8位が「10, 14, 15, 8」です。 2020年11月29日アクセス https://www.ipat.jra.go.jp/ 頼むぞ〜! 結果は… 1位 2番 2位 10番 3位 8番 3歳以上2勝クラス 結果・払戻 | 2020年11月29日 東京7R レース情報(JRA) - netkeiba.com 残念!穴馬候補だった4番と16番が3位以内に入りませんでした。惜しかったですね。 それではみなさまも良い競馬ライフをお送りください。 スクレイピング 、クローリングはアクセス先に配慮してやりましょう。 参考資料 競馬力を上げる馬券統計学の教科書 増補改訂Pythonによるスクレイピング&機械学習 開発テクニック ColaboratoryでSeleniumが使えた:JavaScriptで生成されるページも簡単スクレイピング - Qiita 明日の記事の担当はサーバーサイドエンジニアの寺田さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、デザイナーの本田です。こちらは Enigmo Advent Calendar 2020 の2日目の記事です。 今年で エニグモ へ入社して3年目。昨年末からUXデザイングループへ異動し、 BUYMA のアプリケーション UIデザインがメインになったので、考えることややることが大きく変わった1年だったと感じています。 業務の幅も広がって何を書こうか迷うのですが、今年を振り返ると今までで1番デザインツールと向き合っていたし、学んだことも多かったので デザインツールをXd→ Figma へした話 プロトタイプ作るようになった話 の2つについて書こうと思います。 1. デザインツールをXd→ Figma へした話 デザインデータ引き継ぎの関係上、 Adobe Xdをメインで使っていたのですが、今年の7月頃から Figma へ移行しました。 Xdも使いやすいですし、何より Adobe Creative Cloud ライブラリが使えるので既に登録しているUIパーツはそのまま引っ張ってこれるのでよかったのですが 半年ほどXdを使ってみて、オンラインでの共有がやっぱり使いづらかったのと、社内で Figma を使っている方が何人かいて、使いやすいよ!と言っていただけたのが移行するきっかけになりました。 使いづらかった点を軽くまとめると以下の3つになります。 プロジェクトメンバーで無料プランの方がいるときに共有の仕方を気をつけなければならない クラウド ドキュメントとオフラインドキュメントのファイル管理がどこに置いたかごちゃごちゃになる チーム全体の クラウド ドキュメントが見づらい 頑張れば全然使える範囲ではあったのですが、 Figma に移行してこの3つの課題はすべて解消されたので、移行してよかったなと思っています。 秋頃から有料プランのProfessional Teamにしていただいたので、アプリのUIどうなってたかな?と思ったときにすぐ確認できるようになったし、 Figma 上でコメントしたらプロジェクトごとにslackへ通知がいくように設定したので、エンジニアやPMの方にも Figma の機能使っていただいています。(使いこなしていただいてありがとうございます!) 今後もリモートワーク が続くことを考えると、私個人は Figma をメインにしていくことになりそうです。 Xdのデザインデータが残っているプロジェクトのときはXdを使う、など今後も 臨機応変 に対応できたらなと思っております。 ※でも2年前はSketch使ってたし、1年前はXdでいくぞ!!と思っていたので1年ごとに変わる説はあります。また Figma に代わるサービスが出たときは時代の流れに合わせていきます。 ※SketchとZeplinは解約していただきました。 2. プロトタイプ作るようになった話 昨年までは簡単なLPやUIを作ることが多かったので、プロトタイプをあまり作っていなかったのですが、 プロジェクトメンバーが多いときや長期のプロジェクトになると、認識合わせや動きの確認がイメージしづらいので、プロジェクトによってはプロトタイプをつくるようになりました。 とりあえずすべてプロトタイプまで作ることにすると時間がもったいないなと感じたので、以下の4つに当てはまるときはプロトタイプを作成するようにしています。 モーダルやトーストが表示されるとき ステータスごとにボタンの色やエラー文が変わるとき 3画面以上 遷移するとき スクロールしても画面上部や下部で固定するエリアがあるとき アプリでもWebViewで表示するとき 1.2.3. はチームメンバーとの認識合わせがしやすくなる のと、実機で確認したときにユーザーのことをイメージしやすくなる気がしてます。 4.5. は スマホ のスクロールエリアが確保できているかだったり、不具合がないか確認するためにやっています。 基本的にツールは Figma で作りますが、細かな動きも考慮してデザインしたいときはProtoPieを使っています。 今年は スマホ の ハンバーガ ーメニュー内の動きを作ったのと、トーストの表示タイミングの認識すり合わせをしたいときの計2回ほど作成しました。 10月にリリースした スマホ の ハンバーガ ーメニューのProtoPie制作画面 Figma だと細かい動きはできないことがあるので、ProtoPieも併用していけたらなと思っています。 最後に 今思えば異動してすぐにデザインツールを Figma にしてもよかったし、すべてのプロジェクトでとりあえずプロトタイプ作ってもよかったな〜というのが正直な感想です。 ただ、リモートワーク 下においても半年ほど現状のツールでなんとかできないか試行錯誤した上で、どうしても譲れないポイントを自分の中で見つけられたのは大きな収穫だったかなと思っています。 あとは Figma もProtoPieもアプリデザイナーの先輩方が勉強会を開いてくださって、わからないことがあったらいつでも質問していいよ〜とやさしく声をかけていただいたのもうれしかったです。ありがとうございます! 来年も今年学んだことをベースにさらにいろんなことに挑戦できたらいいな。 最後まで読んでいただきありがとうございました。 明日の記事の担当は、エンジニアの竹本さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。サーバーサイドエンジニアの平井です。 今年もあと1ヶ月ですね。リモートワーク中心の生活スタイルに変わり、より一層時が過ぎるのを速く感じています。 もう年末ということで、弊社では今年もAdvent Calendarを開催します!! 題して、 Enigmo Advent Calendar 2020 です!! 記念すべき1日目は、私、平井の「Cloud Run 使ってみた」になります。 プロジェクトで簡単な API をCloud Run(フルマネージド)上に実装したので、それについて話したいと思います。 構成 Cloud Run(フルマネージド)について 準備 Dockerfile cloudbuild yaml その他 ドメイン 他GCPサービスとの連携 感想 最後に 構成 会員毎にパーソナライズされたコンテンツ情報を返す API をCloud Runを使って実装しました。 とてもシンプルですが、構成図は以下のようになります。 Cloud Run上に Ruby on Rails で API を実装し、Datastoreに格納されている情報を取得します。 Datastoreには、会員毎のコンテンツ情報が格納されていて、この情報はBigquery上に格納されている 機械学習 のデータを元に生成されています。 そして、クライアントから API にアクセスして情報を表示させています。 Cloud Run(フルマネージド) について Knativeを基盤として構築された GCP のサーバーレスサービスの一つで、コンテナをサーバーレスで実行してくれます。 スケーリングなども自動で行われるため、開発者はアプリケーションの開発に専念することができます。 Cloud Runには二種類あり、 Google が管理する Kubernetes 環境で動作するCloud Runと自身の管理するGKEで動作するCloud Run for Anthosがあります。 今回は、より簡単に利用できるCloud Runを使いました。 準備 Dockerfile Cloud Run上で動かすコンテナを用意する必要があるので、 Dockerfileを用意します。 cloudbuild yaml Cloud Runへのデプロイ関連のタスクはcloudbuild. yaml で管理しました。 公式ドキュメント にも記載されているやり方と同じです。 ファイルの中身は、コンテナイメージのbuildとCloud Runへのデプロイなどのタスクが記載されていて、 実際の内容とは違いますが、以下のような感じになります。 steps: # コンテナイメージのビルド - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/PROJECT_ID/IMAGE', '.'] # コンテナレジストリーにpush - name: 'gcr.io/cloud-builders/docker' args: ['push', 'gcr.io/PROJECT_ID/IMAGE'] # コンテナレジストリー上のイメージを使って、Cloud Runにコンテナをデプロイ。 - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' entrypoint: gcloud args: ['run', 'deploy', 'SERVICE-NAME', '--image', 'gcr.io/PROJECT_ID/IMAGE', '--region', 'REGION', '--platform', 'managed'] images: - gcr.io/PROJECT_ID/IMAGE cloudbuild. yaml を作成した ディレクト リによりますが、 gcloud builds submit を実行するとファイルに書かれたタスクが実行され、Cloud Run上にサービスがデプロイされます。 基本的には、Cloud Run上でコンテナを起動するために必要な準備は以上になります。 その他 ドメイン Cloud Runでサービスを作成すると自動でデフォルトの ドメイン が発行されます。 ただ、実際のサービスで利用する際にはカスタムの ドメイン を設定したくなると思います。 Cloud Runには カスタムドメインのマッピング機能 が実装されているので、使いたい ドメイン を マッピング することができます。 実際にカスタム ドメイン を設定してみましたが、ドキュメントを読んで簡単に設定することができました。 他 GCP サービスとの連携 API のデータ取得先として、Datastoreを使用しています。 今回は、Cloud Runのサービス とDatastoreのデータが同じプロジェクト上で管理されていたので、二つのサービスが連携する際に、認証の設定は必要ありませんでした。 感想 実際にCloud Runを使ってみた感想は以下です。 インフラ関連を意識せず開発できて、アプリケーションの実装に集中できた。 デフォルトのメトリクスが充実していた。 リク エス ト数、コンテナのメモリ使用率 ログも充実していた。 キーワード検索、 重要度によるフィルタリング 他 GCP サービスとの連携が楽だった。 最後に 今回のプロジェクトの方針で、なるべく早くユーザーの行動をみたいという背景もあり、Cloud Run上に API だけを実装するという最低限の選択をしました。 今後の長期運用を考えて、 機械学習 のデータから API 用のデータを生成する部分もCloud Run上に乗っけていこうと考えています。 最後まで読んで頂き誠にありがとうございました。 明日の記事の担当は、UXDグループの本田さんです!! 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。インフラグループの夏目です。 前回のエントリー より3ヶ月、エントリー投稿から2週間ほどで無事子供が誕生し育休期間を経て先日復職しました。 今回のエントリーでは実際に育休を取得してみてどうだったのか?といった内容を書いていきます。開発者ブログというお題目にも関わらず育休話が続いてしまい恐縮ですがお付き合いください。 育休っていつから取るの? いざ育休を取るぞ!と意気込んで最初に感じたのが「いつから育休を取ればいいの?」という、開始時期についての疑問でした。 女性の場合出産予定日の1ヶ月少々前から産前休業に入り、予定日が多少前後しても産後休業を経て 育児休業 、という流れになるのでさほど開始時期について悩む部分は少ないと思うのですが、男性は産前休業がありません。 予定日から育休取得開始ということで社内調整はしていたものの、いざ予定日になっても兆候がなかったためなし崩しに「生まれた翌日から休みます」という本人も周囲もどうしたら良いのかわからない宣言後、1週間ほど後ろ倒して生まれたので育休開始、というスケジュールになってしまいました。 実のところ 育児休業 の法令上は予定日から取得可能なため、気兼ねなく育休開始としてもまったく問題ないのですが、生まれていないのに休業して日がな一日何をしていたらいいんだ…?という戸惑いがあり、育休に踏み出せませんでした。 ただ、人事総務的な観点からはいたずらに後ろ倒しをすると給与計算などで面倒な調整が発生するような(というか発生した)ので、やることがあろうがなかろうが当初予定日から変更するべきではなかったかな、と反省しています。 なお予定日から 育児休業 は取得可能なものの、 育児休業 給付金は出産日から起算となるようなので(詳しくは 育児休業 に関する各種法律をご参照ください)、そのあたりも踏まえて調整するのがベターだと思われます。 育休ってどれくらい取るの? いざ育休が始まり、体力がまだ本調子でない妻のサポートをしつつ日々の育児におおわらわになって1ヶ月が経過したあたりで、次は「これ育休期間2ヶ月の予定だったけど全然足りないんじゃないか…?」という疑問が生じました。 新生児期は授乳やおむつの回数が多く手がかかるから、大変な時期を乗り越えれば…と思ったのですが、乳児期になっても手がかかることには変わりなく、大変じゃない時期など存在しません。 たとえば「首が座るまで」といった身体的な発達状況を目安にするのもわかりやすくて良いかなとは思ったのですが、いつ頃にできるようになるかはまちまちで、「復職時期は子供次第です」という状態は自分も周囲もどうしたら良いのかわからなくなってしまいます。 自分の場合は結局1ヶ月半頃に不安を感じた妻から「もっと伸ばせないの?柔軟に調整できるんでしょ?」と聞かれたものの「いや2ヶ月って一応決めてたし仕事の勘も忘れちゃうから…」とやや強引に押し切って当初予定通り2ヶ月で育休終了としました。 これは振り返ってみれば良い判断ではなかったと反省しています。生まれた時期や家庭環境によってまちまちになるかとは思いますが、極力調整して取れる限り取るのが家庭内平和のためにも最善です。 育休取ってみてどうだった? 当初育休を取るぞ!と決意したときの自分のスタンスは「妻には育児に専念してもらって、他の家事は全部自分が巻き取る」という、振り返ってみれば寝言のような方針だったのですが、当然ながら育児を1人でやりおおせるわけがなく。 2人がかりで育児の合間にそれぞれできる範囲で家事をするという状態がしばらく続き、育休終了1週間前くらいからワンオペ状態を意識して徐々に分担して、という形で慣らし運転をして育休を終えたものの、生活のスタイルについては現在も模索しているところです。 近隣にサポートしてくれる家族や友人がいたり、里帰り出産をするのであれば負担も軽減できるかと思いますが、 核家族 な我が家の事情と現在の社会状況を鑑みると、 育児休業 を取って(取れて)本当に良かったなと感じています。 人によっては出産後数日や1週間程度ちょっと手伝う程度というパターンも少なくないとは思うのですが、自分の場合は子供と接している期間が短いといざというときに勝手がわからず、足手まといになったであろうことは想像に難くありません。 また、取得前に予期していた通り…というか予期していた以上に生活のすべてが子供を中心として回るようになり、そのことに慣れるためにも育休期間は大変有用でした。 業務面ではどうだった? 育休取得前のエントリーではカオスエンジニアリングが云々と言っていましたが、メンバーが一人抜けた程度で大きな支障が出るわけもなく、引き継ぎが十分だったかどうかはさておき概ね問題はなかったという認識です。 自身の関わっているプロジェクトがある程度峠を越えたタイミングだったこともありますが、それ以上にタスクを引き継いでくれたチームメンバーがよしなに捌いてくれた部分が大きく、あらためて環境に恵まれているなと思いました。 他方で、リモートワークが定着したことと育児の疲弊からついつい社内Slackを眺めてちょっかいを出してしまうことが二度三度あり、これについては 労務管理 の観点からは褒められた行為ではないため、業務への未練をすっぱり断ち切る強い心が必要だという学びがありました。 準備は足りた? 以前のエントリでは必要なものをSpreadSheetで管理、TodoをTrelloで管理してこれで十分…と思っていたのですが、準備はまったく足りていなかったため現在進行系で色々と公的な申込み手続きをしたり育児グッズを買い足したりしています。 育児ブログではないので仔細には書きませんが、大雑把に導入して良かったものを以下に挙げます。 導入して良かったもの ベビーモニター 大きな戸建て住宅に住んでいるわけでもなし、狭小マンションで目の届くところにいるから必要ないかと思っていました。しかし、逆に生活圏内に子供がいるからこそ生活音や照明に気を使う日々に疲れ果て、適切な距離を保ちつつ様子を見るためにRaspberryPiで ベビーモニター を作りました。 RPi Cam Web Interface というWebインタフェースを導入したため、市販の ベビーモニター と違って専用のモニターユニット不要でPCや スマートフォン でどこからでも子供の様子が見れる、という完璧なソリューションが実現しました。 育児記録アプリ:ぴよログ 授乳時間や睡眠時間などありとあらゆる育児項目を夫婦間で記録共有・グラフで可視化できるのはもちろん、 Apple Watch の ウィジェット からも記録ができるといった便利さで、このアプリなしでの育児は考えられないほどです。 ベビーベッド側面から俯瞰する形でカメラを取り付けています。カメラとは別の位置から赤外線LEDも照射しているため、夜間でも様子が見えて安心です 気軽に 育児休業 を取ろう なぜ今「男性の育休義務化」が必要なのか?フランスでも「男性の産休」義務化の動き 「男性の育休」は実はメリットだらけ。取得期間や助成金などの制度を知って活用しよう 産後女性の死因の一位は自殺。「産後うつ」と男性育休の関係、専門家が語ったこと。 家庭によっていろいろと事情はあるかと思いますが、上に挙げたように社会的な潮流や時勢からもこれまで以上に育休を取ることが推奨され、社会制度も整備されて金銭面の不安も軽減されていくことと思われます。 デメリットよりもメリットが多く、家族で濃密な期間を過ごせるのは間違いないので、ぜひ気軽に 育児休業 を取得されてみてはいかがでしょうか。 ちなみにこれはまったくの余談なのですが育休の2ヶ月間で3kg痩せました。日々成長する ダンベ ルを使っていると強制的に鍛えられるので健康面でも育休はおすすめです。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。インフラグループの夏目です。 今回は技術的な話ではなく、ちょっとプライベートに込み入った話というかエンジニアの ワークライフバランス 的なお話です。 まったくの私事なんですがこのたび子供を授かることになりまして、ついては 育児休業 を取ってみようかな?ということで、育休取得までの流れと業務面でどういった対応をしていったのか、といったことを書いていきます。 育児休業 を取ってみよう 今回育休の取得に至った経緯は色々とあるのですが、ひとつに ワークライフバランス を見直す良いきっかけになるかな、という意図があります。 時短勤務やリモートワークではなく一旦完全に業務から離れてみて、日常生活における育児の占める割合はどの程度のものなのか(全部だよという意見はごもっともです)ということを理解しておくべきだろう、と。これを把握せずに、妻が産休・育休で子供の面倒を見るのに対して自分だけこれまでどおりの働き方をしていくのは、先々を考えると良いスタンスではないように思えたため、第一子のタイミングで育休を取ることにしました。 エニグモ では 裁量労働 のメンバーが多いこともあり、育児都合で勤務時間を調整する様子が日常的に見受けられます。このため、いざ自分に子供が生まれますよということになっても、育休を取得すること自体は自然と受け入れてもらうことができました。 受け入れるも何も労働者の権利の行使なので、本来拒否権はないはずでしょうという話なのですが、そうもいかない会社が多くあることもまた事実です。この点については エニグモ の風土に助けられた部分が大きいなと感じています。 取得までの流れ さて、では実際に育休を取りましょうとなってからどういった行動を取ったか、時系列に沿って大雑把ですが書いてみます。 2019年10月に妊娠確定となり、ここから家庭内協議に入りました。自分の稼働状況など鑑みて育休が果たして取れるのか、取れるのであればどれほどの期間で収入面で問題がないのか、などについて妻と喧々諤々やり合いました。 法制度の話をするのであれば1年以上取得することも可能ですが、現実問題として夫婦共に育休となった場合の収入への影響は無視することができず、第一子で不安を抱える妻に多少折れてもらいつつも2ヶ月取得してみましょう、ということになりました。 結論が出たのが11月末、ひとまずチームリーダーへ相談です。自分が アサイ ンされているプロジェクトの今後の見通しや、その他担当タスクのチームメンバーへの委譲可否などを考慮のうえ、積極的に育休を取得してほしいとの言葉を頂きました。チーム内の調整は大丈夫(な状態にするしかない)となったので、後は部長や人事総務の方に掛け合うだけ、というところまで持っていきました。 年が明けて2020年の2月、自身のタスクを育休までにどう調整するかといった大まかな線表を作成して部長へ共有し、社内の具体的な育休取得手続きに入ります。 というタイミングで、全世界的に新型コロナウィルス 感染症 が広がり、 エニグモ でもリモートワークに突入し手続きはいったいどうすれば…?という状態になってしまいました。幸い事前手続きはほぼないことと、書類のためにわざわざ出社する必要なく郵送してねということで柔軟な対応をして頂けました。ありがたい限りです。 タスク調整どうしたの タスク調整は実際問題どのようにしたかという点については特筆すべきことはなく、予定日までに大きなタスクを終わらせつつドキュメントを整理したり、自動化可能なタスクは自動化、間に合わない場合は手順書を作るといった形で粛々と対応を進めました。不穏当な表現ですが、トラック係数やバス係数といったものをあらためて意識することができました。 また、カオスエンジニアリングとまではいきませんが、どれだけ自分が準備をして引き継ぎをしたところで不測の事態は発生しうるので、そのあたりはチームメンバーになんとか拾い上げてもらうしかないかな、と自分勝手なことを考えています。 私生活で準備したこと 育休とはあまり関係なく、プライベートで準備したことについては基本的に世間の皆さんと同じです。強いて言えば、以下のようなエンジニアチックな手法で試行錯誤をしています。 書籍や雑誌などから育児に関する必要なものリストをピックアップして Google SpreadSheetへ書き出し、購入期限や数量、消耗品か否かといったメタ情報を付与して購入状況を管理 公的な手続きなど、忘れがちなものはTrelloのカンバンを作って 進捗管理 Trelloのボードイメージ おそらくこういったことに対応するための専用アプリやサービスもあるかと思うのですが、それらの使い方を覚えている暇があったら使い慣れたツールを使ったほうが早いだろう、と横着をしている状況です。 今後の見通し 前述したように雪崩式にリモートワークとなり、当初想定していた育休へのロードマップとは少々異なる現状なのですが、反面リモートワークでも業務への支障はほぼゼロに近いということがわかりました。2ヶ月間の育休取得後に世間がどういった状態になっているのか(おそらくあまり変化はないでしょうが)推測できませんが、リモートワークへの敷居が下がったこともあるので、たとえば保育園に入れるまでは セミ リモートワークや半育休といった形態で、柔軟な働き方を選ぶこともできれば良いなと思っています。 このエントリーを書いてる今現在、出産まであと数日といった状況です。2ヶ月後にこのエントリーを読み返して「全然準備できてへんやんけ!なに余裕ぶっとるんじゃ!」となりそうな気配が漂っていますが、2ヶ月後に改めて 育児休業 の間に起きたことや感想についてのエントリー投稿を予定していますので、そちらをお楽しみに。 結びに 少々自己矛盾してしまうのですが、冷静に考えるとこのエントリーのタイトルは筋が悪いなという印象があります。こんなタイトルのエントリーが企業ブログのバリューとならず、掃いて捨てるようなエントリーとして扱われる時代にしていきたいものですね。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。インフラグループの夏目です。 エニグモ ではメインのGitサービスとしてGitLabを使って ソースコード を管理しています。 GitLabは GitHub と同様に、IssueやMR(PR)にラベルを付与して作業の優先度やステータスを表すことができるのですが、このラベルの運用でちょっと困ったことが発生して泥臭く解消するはめになったので、経緯と顛末含めてご紹介します。 プロジェクトラベルとグループラベル GitLabのラベルは、プロジェクト( リポジトリ )とプロジェクトを束ねたグループとでそれぞれ個別に定義できます。 グループラベルとして定義したものは配下のプロジェクトでも使用できるため、基本的にはグループ側で汎用的なラベルを設定して、プロジェクト固有のメタ情報としてプロジェクト名の省略形(例: Enigmo Greatest Project → EGP )や、リリースバージョン(例: v1.4.3 )などをプロジェクトラベルで定義するというのがベターな運用方法だと思われます。 グループラベルを設定しよう ところがこのグループラベル、 エニグモ ではあまり認識されていなかったため 各プロジェクトで汎用的な名前のプロジェクトラベルを個別に設定 プロジェクトラベルを使ってIssueやMRを管理 プロジェクトごとに同じラベルを定義するのが手間だったので、プロジェクトラベルと同じ名前のグループラベルを設定 という流れで最近になってようやくグループラベルが設定されました。各プロジェクトは個別にラベルを定義する必要がなくなってめでたしめでたしかと思いきや、そうではありません。さきほどのフローに妙なところがありませんでしたか? プロジェクトラベルと同じ名前のグループラベルを設定 ここです。同じ名前のラベルを設定しましたね。グループラベルとプロジェクトラベルは別のオブジェクトとして扱われるため、同じ名前のラベルが設定できます。設定できるのは問題ないのですが、いざ使ってみようとすると こんな感じでまったく区別のつかないラベルがラベル付与の候補として表示されるようになってしまいました。どうしてくれるんだ。しかも 厄介 親切なことにこのラベル候補、グループラベルかプロジェクトラベルかという情報は表示されません。 重複したらどうする? 区別がつかなくても同じ意味合いだったら別にどっちでもいいじゃん、と思わないでもありませんが、ラベルを付与するたびに「なぜ2つあるのか?どちらを付与したらいいのか?表示のバグ?」とチラッと考える時間が無駄ですね。じゃあこの重複を解消しましょう、と迂闊に既存のプロジェクトラベルを削除してしまうと、これまで該当のラベルを付与していたIssueやMRからラベルが削除されてしまいます。 仮に review requested などのアクションが必要なラベルの場合、削除によって対応が漏れてしまうおそれがあります。この事象に対して、 GitLab.orgのIssueで対応方法 が提案されていました。 重複しているプロジェクトラベルかグループラベルをリネームする プロジェクトラベルが付与されたIssueやMRに、グループラベルを追加する プロジェクトラベルを削除する たとえば in review という名前のプロジェクトラベルとグループラベルが設定されている状態では、以下のような対応になります。 プロジェクトラベル: in review を in review(project) へリネーム in review(project) が付与されたIssueやMRに、グループラベル: in review を付与 プロジェクトラベル: in review(project) を削除 リネームと削除はグループ設定画面やプロジェクト設定画面から容易に対応できますが、活発に開発が行われているプロジェクトにおいて、Issue一覧やMR一覧で重複しているラベルが付与されているものを探してIssueやMRの編集画面でラベルを設定する、という流れを1つ1つ対応するのはそれこそ時間の無駄です。 API を使ったラベルの修正 幸い、GitLabではプロジェクトラベルやグループラベル、MRを操作する API が提供されています。 Labels API | GitLab Merge requests API | GitLab これらの API を利用して以下のような スクリプト を作成しました。 #!/bin/bash # require # - httpie # - jq # - GitLab Administrator Role & User Token TOKEN=************ total_pages= $( http --ignore-stdin 'https://gitlab.example.com/api/v4/groups/1/merge_requests?labels='"review requested(project)"'&per_page=100' Private-Token: $TOKEN --verbose | grep X-Total-Pages | awk { 'print $2' } | sed -e 's/ //g' ) for page in $( seq 1 ${total_pages}) do http --body --ignore-stdin \ 'https://gitlab.example.com/api/v4/groups/1/merge_requests?labels='"review requested(project)"'&per_page=100&page=' ${page} '' \ Private-Token: $TOKEN | jq '.[]|{ id: .id, iid: .iid, project_id: .project_id, labels: .labels}' done > request.json jq -c '.' request.json | while read mr do id= $( echo ${mr} | jq '.id' ) iid= $( echo ${mr} | jq '.iid' ) pid= $( echo ${mr} | jq '.project_id' ) labels= $( echo ${mr} | jq '.labels|join(",")' ) replaced_labels= $( echo ${labels} | jq -r 'sub("review requested\\(project\\)";"review requested")' ) http --body --ignore-stdin \ PUT 'https://gitlab.example.com/api/v4/projects/' ${pid} '/merge_requests/' ${iid} '?labels='" ${replaced_labels} "'' \ Private-Token: $TOKEN sleep 5 done 今回は重複しているラベルが少なかったため、ラベル名は引数ではなく直接定義してしまっています。大雑把には以下のようなフローです。 対象グループ内でリネーム後のプロジェクトラベルが付与されたMRの一覧(ラベル定義の変更に必要なメタ情報を含む JSON )を作成 ラベル定義を修正した JSON を使って各MRの情報を更新 スクリプト 内ではラベルの置換をしていますが、前述の対応方法に記載されているようにプロジェクトラベルを削除することで更新されるため、置換ではなく単純な追加でもOKです 各プロジェクトのラベル一覧を確認して重複しているラベルがあったら スクリプト を適宜修正して実行、という対応で重複を解消しました。 すべてのグループやプロジェクトに対してラベル重複のチェックをして解消したい場合は、以下のような各言語向けのモジュールを使った スクリプト を作成するのも良いかもしれません。 Python: python-gitlab Ruby: gitlab Golang: go-gitlab Node.js: gitbeaker 結論 汎用的なラベルはグループラベルに定義すること 既存のラベルと同名のラベルを適当に作らない 実際にラベルを使う場面をちゃんと考えましょう 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
はじめに こんにちは、初投稿になります。 今年の1月から エニグモ でデータサイエンティストをしている堀部と申します。 1/25(土)に行われたCA × atmaCup というオフラインのデータ コンペティション のイベントに参加しました。 【東京・大阪同時開催】CA × atmaCup - connpass 85チーム中、Public 12th/Private 5th *1 とKaggle Expertとしてはまさかの最上位という結果でした。 コンペ前、コンペ中、コンペ後に分けて振り返ります。 ※本記事ではコンペのデータやタスクについては情報公開が難しいため触れません。 コンペ前 コード整理 以前のコンペや業務で使っていたコードをなるべくクラス・メソッド化してgitにまとめて整理しました。綺麗なコードとは言えないですがまとめておいたおかげで、新しい特徴量の作成やタスクに特化したデータの前処理に時間を割くことができました。主に汎用的な特徴量生成の処理やGBDTモデルの実験をやりやすくする下記のようなコードをまとめていました。 EDA trainとtestの特徴量の重なりをベン図で表示 https://github.com/konstantint/matplotlib-venn 特徴量生成 1種類しかないカラムを削除 集約特徴量の生成(mean, median, max, min, std) label encording count encording target encording 学習(モデル) ベース:lightgbm 5-fold オプション:random seed averageやlightgbm tunerをオンオフできるように追加 コンペ中 素敵な環境でした サイバーエージェント さんのできたてほやほやのオフィスの スクラン ブル交差点の見える席でもくもくと作業しました。頭を使いすぎて最後2時間くらいは少し頭がぼーっとしてました。 コンペティション サイトの出来が素晴らしかったです。○aggleと違ってすいすいsubmitができストレスなく作業に集中することができました。 コンペ中の作業方針 必要最低限の特徴量でベースライン作成してsubmit EDA やfeature importanceを見て特徴量追加 CVもpublic LBも伸びたら採用 ひたすら、2,3を繰り返す モデルはずっとlightgbmシングルでアンサンブルはしませんでした。lightgbmのハイパーパラメータはcat_smoothのみ手動チューニングしました。 そして、特徴量のアイディアが一旦つきたところで lightgbm tunerを使って一度パラメータ探索をした結果を最後まで使いました。 コンペ後 表彰式、懇親会 コンペが終わってすぐ結果発表&表彰式がありました。上位3名の方の解法を聞くことができました。 その特徴量効きましたよねと共感したり、そのアイディアは思いつかなかったなとメモしたりすぐに情報共有できるオフラインコンペならではのよさを実感しました。 その後の懇親会でも当日のコンペだけでなく別のコンペについての取り組みについても話を聞くことができ、普段からコンペに参加しておくメリットも感じました。 次の日 次の日は日曜日だったので表彰式や懇親会での話を受けて思いついたアイディアを実装して追試をしていました。少し考えればこの特徴量は要らないかもと思ったものを削除しただけで、スコアが伸びたりとなぜコンペ中に気がつかなかったのかと後悔もありました。 また、上位の方が公開してくれたコードやgitを見て自分のコードの汚さに愕然としました。 普段から綺麗なコードを書く、他の人のコードを見て学ぶということを日頃から意識しようと胸に刻み込みました。 最後に (真剣に取り組んだ)コンペの数だけ強くなる、強い人に会いに行くという大切さ、オフラインコンペならではの大変さ、楽しさを味わうことができたとても充実した一日でした。次回があればまたぜひ参加したいです。 また、 エニグモ に入社して1ヶ月が経ちましたが楽しい日々を過ごしています。ABテストの効果検証×傾向スコア、商品の クラスタリング 、回帰予測 モデリング など、自分で思いついてビジネスに貢献できそうなものであれば自由に取り組んでよい ボトムアップ の環境です。 個人的には、 ファッションEC × CtoCという ドメイン ならでは多様性のあるデータ(テーブル、テキスト、画像)が扱えること 社内の半数以上の方が SQL をかけデータをビジネスに活用する文化が定着していること BigQuery, AWS , Lookerなど、ツールへの投資が当たり前のように行われていること によさを感じています。 コンペ以上に業務も楽しく取り組める強いチームをつくれたらと思っております。興味のある方はぜひ気軽にご応募ください。選考とは別にカジュアル面談も受け付けております。データサイエンティスト、データアナリスト、データエンジニア、検索エンジニアなど各種募集中です。 株式会社 エニグモ 正社員の求人一覧 hrmos.co *1 : ランキングシステムがPublicとPrivate で分かれていて、 コンペティション が終了するまではテストデータの一部だけを使ってスコアを算出されたPublicのみのランキングが公開されます
アバター
こんにちは。主に BUYMA の検索周りを担当しているエンジニアの伊藤です。 BUYMA ではSolrを利用した検索システムがいくつかあります。 BUYMA の検索というと検索ボリュームが一番大きな商品検索を想像されると思いますが、 今回はデータボリュームが一番大きい検索システムをターゲットとして、インフラ周りを含め全面的にシステムの刷新を行いました。 ここでは、 既存の検索システムがどういったものだったのか なぜシステム更改が必要だったのか(どういう課題があったのか) 更改後の検索システムはどういったものか 今後の課題について 等々についてご紹介したいと思います。 既存の検索システムについて 既存の検索システムは下記の通り、シンプルという点ではとても素晴らしいものでした。 ただし下記のような問題を抱えている状況でした。 スケールアウトしない構成である スケールアップの限界 Solrのバージョンが古い 本番環境のデータを利用した検証が難しい 開発担当者にもSolrの知識が必要とされる 問題に気付けない 以降でその内容について補足していきます。 スケールアウトしない構成である 検索システムはデータ増とアクセス増の2軸でスケールアウト構成を取る必要があります。 アクセス増にはSolrサーバを追加することで対応できるようにはなっていましたが、該当の検索システムはアクセス数というよりデータが多いことが特徴でこのデータ増に追従出来なくなっていました。 スケールアップの限界 該当の検索システムは元々オンプレ環境で運用していましたが、ヒープサイズ不足やFull GC が頻発するようになっていたため、暫定対処として AWS 側に移行し、 GC チューニングを行いました。 ただしこの対応も一時的なもので、データ数自体は日々増加している状況の中で使用している GC の アルゴリズム 的に、メモリを増やしてどうこうできる状況ではなくなっていました。 データ増に伴いOOM Killer、ヒープサイズ不足が発生 → ヒープサイズを拡張 → STWの インパク トが大きくなるという負のループに陥る Solrのバージョンが古い 既存の検索システムで利用しているSolrのバージョンは3.2.0でした バージョンが古くても通常時の運用にはそれほど支障はありませんが、何か問題があった場合の調査が大変でした(ドキュメントが見つからない) またSolrのバージョンupに伴いindexing/検索性能はupしていますので、性能面でのデメリットであったり 以降のバージョンで提供された様々な機能が利用できない等のデメリットがありました 本番環境のデータを利用した検証が難しい 既存システムで本番 環境相 当のデータで検証する際にはindexing処理だけで数日かかるという状況で、環境構築自体がハードルとなっていました。 開発担当者にもSolrの知識が必要とされる Solrへの検索時は php 、 ruby といったクライアント側でSolrのクエリを組み立てて実行していました。 クライアント側を担当する開発担当者はその時々で変わり、ある程度はSolrを知っている人もいれば、全く知らない人まで様々です。 一方Solrのクエリは書き方によってはキャッシュを使えていなかったり、または逆にキャッシュを無駄に汚してしまったりと、同じ検索結果を得るにしてもある程度Solrの知識を必要とします。 問題に気付けない 必要なデータやログが適切に可視化され監視されていないため、サービス影響、ユーザ問い合わせがあって初めて問題に気付くケースがありました 更改後の検索システム 上記で挙げた課題感を解決するために設計した アーキテクチャ が下記になります。 変化に強いシステムへ SolrCloud構成に変更しつつ、データ増にはデータsharding、アクセス増にはデータreplicationでスケールアウト可能な構成に変更 環境再現性 全てコンテナベースのアプリケーションとなっているため、本番と同じマシンスペック、構成を簡単に構築可能 本番データを利用した検証も短時間で可能となった indexing処理についても BUYMA で利用しているメインのDBにアクセスする必要がなく、中間DBを利用することで数時間でfull-import可能 耐障害性の向上 プラットフォームに Kubernetes を採用し、想定外のアプリケーション停止、ノードdown時にもセルフヒーリングで自動復旧可能 マイクロサービス化(隠蔽化) アプリケーション〜Solr間に検索 API を新たに用意。これによりアプリケーション側はSolrの独自クエリを意識することなく、検索 API を介したシンプルなI/Fをベースとした開発が可能に。 Solrの独自クエリなど、専門的な知識を開発者が持つ必要がなくなり、学習コストが抑えられることで開発効率が上がる 不適切なクエリにより、Solr側のキャッシュをうまく使えないなどの問題回避 障害検知 監視、ログ運用基盤としてDatadogを利用し、インフラ/ミドル/アプリケーションのメトリクス情報やログを統合的に管理可能とした 属人性の排除 検索システム間でバラバラだった スキーマ 定義を汎用的なものにし、複数の検索システムをシームレスに対応可能に dynamicフィールドをベースとしているため、 スキーマ 定義自体も運用上ほぼ変更する必要がない GitOps対応 全てのシステム設定をコードとしてGit上で管理 運用上変更頻度が高いものはコマンド実行不要でCI/CDで可能 苦労した点 今回せっかくシステム更改するならCloudNativeなシステムにしたいという思いがありこのような構成にしました。 Solrのようなstatefulなアプリケーションをコンテナとして Kubernetes 上で扱うのも初めてでしたし、 監視、ログ運用周りも今回の対応と合わせて刷新したことで対応範囲が広くなり苦労した点も多くありました。 ここではいくつかピックアップして紹介したいと思います。 Solr/Zookeeperを Kubernetes 上に構築、運用した経験がない Solrに関してはあまり構築事例もなく問題に遭遇した際の解析に難航 リソース設計、ログ周りなど、コンテナ/ Kubernetes だからこそプラスαで考慮しなければ行けない点が多々あった 監視、ログ運用基盤の検討 コンテナ/ Kubernetes ベースのシステムをどう監視すべきか? ログはどう扱うべきか? 検索 API の設計、開発 I/F仕様どうするか? 開発しながらSolr/Zookeeper周りの設定や問題対応をするのが辛い Solrバージョンアップによる未知の問題対応 indexing処理周りで問題多発 SolrCloudかつDIHで並列indexing処理を行うこと自体が事例がなく、色々とはまる 今後に向けて システム更改により当初挙げていた課題感というのは概ね解決することが出来ました。 ただし今回のようなシステムを構築、運用したことでてきた新たな課題や、まだまだ改善が必要だなと思えるところもあります。 indexing処理などはクライアント側の処理と密になっている箇所がまだまだあり、今後もブラッシュアップが必要 Solrのような複雑かつstatefulなアプリケーションは運用がやはり大変。 Kubernetes operator を利用するなどして運用をもっと楽にしつつ、属人性もなくしていきたい。 さいごに 今回は検索のインフラ面を中心としたお話を紹介させていただきましたが、 弊社では検索のサービス向上(ex パーソナライズサーチなど)や 自然言語処理 や 機械学習 を活用したサービス導入なども積極的に行っています。 今回技術的なところにはほぼ触れませんでしたが、ここどうなってるの?など少しでも興味、関心を持たれたら是非お気軽にお話だけでも聞きにいただければと思います。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
はじめに エニグモ でデータサイエンティストを名乗っている庄子です。こちらは Enigmo Advent Calendar 2019 の25日目の記事です。 今年の振り返りも兼ねてのポエムとなります。 さて、データサイエンティストが活躍するためのスキル要件として、いくらでも切りようがあると思いますが、特に自分自身に感じている課題について、4つの力という観点で書きたいと思います。 その1 提案力 PoCとして小規模のデモを行う そのデータサイエンスのアウトプットが使えそうか、事業に詳しい人に想像してもらう 実際にデータサイエンスを使って問題解決できそうな場合も、実際にやってみないと分からないですし、得られたアウトプットが事業に有効かどうかを、事業に詳しい人に意見をいただいた方が良いでしょう。筋が悪そうな分析は早めに判断してもらうためにも、なるべく小規模でPoCを行います。 本年度の振り返りとしては、このPoCの手数がもっとあった方が良いと感じました。 また、データサイエンティストが使う手法でできることは、データサイエンティストがもっとも良く分かるはずです。もちろん、使われる手法に関して詳しい人がデータサイエンティスト以外のポジションを担っているケースもあると思いますが、そのケースはここでは考えないことにします。 なお、課題設定が済んでおり、入出力が決まっているテー ブルデー タに対しては、AutoMLを十分ですし、AutoMLに変えられるようなタスクをこなすだけであれば、データサイエンティストは価値を発揮できません。 課題設定と分析を繰り返し、仮説検証を繰り返しながら課題設定の質を上げるスキルこそがデータサイエンティストの価値を差別化すると思います。 提案を誤解なく伝えるために、共通言語を関係者と揃える 使う手法や方法論を伝える概念を伝える 伝えたことをドキュメントとして残し、説明の場にいなくてもそのドキュメントを読めば共通言語が理解できるようにする 関係者が運用している具体的なデータを使って示す 共通言語を揃える場合は、最初に自分が受ける説明として、こんな説明ならすっと理解できそうというのを思い出しながら、説明するということは普段気をつけているつもりでしたが、後から考えると考慮が不十分だと思うことは多々ありました。特に、背景や経緯を共有できている前提から資料を作ってしまう場合です。 分析レポートの報告の際に、なるべく普段気をつけていたはずだと思いますが、ドキュメントの作成や整備も、振り返るともっとやりようがあると思います。 質問された点について補足を付け加えていくなどして、社内のナレッジの質を高め、 リテラシー を共有できるのかと思います。 その2 ヒアリ ング力 定期的に情報交換の場を現場のメンバーと設ける こちらの取り組みを参考にし、ざっくばらんにデータ活用で解決してほしい課題について話すという ランチミーティング を設定しました。 データ民主化を加速させる「分析ワクワクタイム」 - pixiv inside この ランチミーティング をきっかけにとあるPoCに取り組みましたが、中途半端なところで頓挫してしまっている状況です。 さらに、定期的にということも実現できていません。やったら効果的かもしれないと思うことをすぐに実現できず、せっかく ヒアリ ングしたのに形にできないということに負い目を感じてしまった部分もあります。 いずれにせよ、定期的にコミュニケーションを行うことで、風通しをよくする、 心理的 安全性を担保することが重要だったりするので、またどこかの機会で復活させていきたいと思います。 現場での運用について体験させてもらう 今年は取り組めなかったですが、ビジネス理解をもっとも得られる方法が実際に体験することにあると思います。データサイエンティストが共通言語の理解を得にいくということで矢印の向きは反対になりますが、先に挙げた共通言語を揃えるということにもつながると思います。 その3 タスク依頼力 得意な領域に専念し、タスクを切り出してより得意な人に任せることを考える 実験的なコードを書くことに場合に専念する 仕様が固まる、あるいはプロダクトに関わる部分はエンジニアに任せる プロダクトに乗るべきコードの要件はここで詳しく触れませんが、手元で自分が扱うコードが読みやすく、条件や機能の拡張がしやすいものだったら良いなと何度も思いました。そのようなコーディングを行うためには言語や フレームワーク のベストプ ラク ティスを踏まえている、専門家に任せた方がよいです。 データサイエンスに関するプロジェクトを実行するにあたり、必要なスキルで全てに秀でるのは多くの人にとって現実的ではないでしょう。依頼をする内容を、その領域を詳しい人へ誤解なく橋渡しができるくらいの知識があるという前提は置きますが、なるべく、得意な領域に専念した方が、組織としてのアウトプット力は上げられるはずです。 個人の領域として広くやれるのが弊社の良いところでもあるのですが、実験のためのコードを書く時間の割合を多くすることが、現在ネックになっていると思いました。 タスクを分解する 分解した単位で、インプットとアウトプットを明確にする こうやって書くと、基本的な仕事の進め方の話になりような気がします。。達成基準が明確な、細切れなタスクに分解できるということは、仕事の依頼もしやすくなり、今後育成という観点でも必要になってくる力です。 その4 採用力 来て欲しい人に、その会社に来る魅力をきっちり伝える 具体的なタスクや、データサイエンティストの要件を上手く伝える 人的リソースを増やすために、データサイエンティスト自体の人員を増やすということも当然選択肢に入ると思います。今年度は、データサイエンティスト、および、データアナリストの採用にも、面接などを通し関わらせていただきました。 もちろん活躍できそうな人を採用することが目的になりますが、活躍を期待できる人材ほど、他社からも内定が出ることは容易に想像できます。給与といった待遇面以外で考えるとすると、仕事内容やその会社で働くことの魅力を伝えるかどうかにかかっていると思います。 そこで、ミスマッチをなくすということに務めてきました。課題と思っているとこと、なるべく包み隠さず伝えたということはできていたと思います。実際に業務を行うイメージを固めるために、面接官以外のメンバーとの面談の場を設けることも実現することができました。 応募者の良いところを引き出して聞き出す 会社のフェーズを考えると、データサイエンスに関する施策をドライブしてくれる人が活躍できそうという思いがあったため、提案力に優れている人を通す傾向が強かったですが、応募要件を上げてしまう大きな要因となっていたような気がします。 今後は、始めは提案が少々不得意でも、技術力を活かして活躍できる人も採用したいという想いがあります。課題はたくさんある中で、その人の経験と相性が良さそうなタスクの質問をして、考えを掘り下げる、という質問の組み立てについても、改善していきたいと思います。 一方、できていた部分としては、データサイエンティストの業務経験が想定より不足していても、社内のメンバーとコミュニーケーションが安心して任せられ、学習やそれまでの業務への取り組みを伺い、間違いなく必要なスキルはキャッチアップできると判断できる方は面接を通していました。このケースでは応募者の良いところを上手く引き出せていたのではないかと思います。 さいごに その1,2がコミュケーションに関する話題、その3,4が人的リソースに関する話題になります。大きな枠組の話題としては、如何にスキルや知識をアップデートするか、という日々のインプット・アウトプットも挙げられますが、また機会があれば書くかも、、しれません。 なお、今回できていないことを中心に書いてしまったせいか、ちょっと 胃もたれ しそうな気分になりました。ただし、やりたいのにやれてないことが多いということは、伸び代がすごい!とも言えるのかと思います。一つ一つやるべきことに取り組んでいくしかしないですね! こんな私でも暖かく受け入れてくださり、(きっと)裁量を最大限に与えてくれる弊社なので、共に切磋琢磨してEnigmoをグロースさせたい仲間を絶賛募集中です。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、サーバーサイドエンジニアの平井です。 こちらは、 Enigmo Advent Calendar 2019 、24日目の記事です。 昨年の1月に エニグモ に インターン として入社してから一年が経とうとしています。早いもので、新卒の肩書きもそろそろ無くなってしまいますね。 今回は、 Rubyによるデザインパターン を読んで、 デザインパターン を勉強したので、そのアウトプットをさせていただきます。 タイトルの通り、 デザインパターン について実際の使用例を探してみました。そのパターンと使用例は以下になります。 Strategyパターン Warden Observerパターン rails-observers Iterator パターン actionpack/lib/action_dispatch/http/mime_type.rb Builderパターン mastodon まずは、Strategyパターンから説明します。 Strategyパターン Strategyパターンとは メソッドの中に溶け込んでいる アルゴリズム を別クラスとして分けて、切り替えができるようにするパターンです。 サンプルコードを利用し、悪い例を修正していく形で、さらに、Strategyパターンについて説明していきます。 適用前 class Report def initialize @title = ' 月次報告 ' @text = [ ' 順調 ' , ' 最高の調子 ' ] end def output_report (format) if format == :plan puts( "#{ title }" ) elsif format == :html puts( " <title> #{ @title } </title> " ) else raise " Unknown format: #{ format }" end @text .each do | line | if format == :plan puts(line) else puts( " <p> #{ line } </p> " ) end end end end 問題点としては以下が挙げられます。 output_reportメソッドについて、formatの種類が増えるにつれて、メソッドが長くなる。 Strategyパターンを適用すると、以下のようになります。 class HTMLFormatter def output_report (title, text) puts( " <title> #{ @title } </title> " ) text.each do | line | puts( " <p> #{ line } </p> " ) end end end class PlainTextFormatter def output_report (title, text) puts( "#{ title }" ) text.each do | line | puts(line) end end end class Report def initialize (formatter) @title = ' 月次報告 ' @text = [ ' 順調 ' , ' 最高の調子 ' ] @formatter = formatter end def output_report @formatter .outpout_report( @title , @text ) end end report = Report .new( HTMLFormatter .new) report.ouput_report このように、同じ目的(レポートをフォーマットする)を持ったオブジェクトを、ストラテジとして定義し、そのストラテジをごっそり交換できるようにするのがStrategyパターンです。ストラテジは同じインターフェースを持っているので、ストラテジの利用者(コンテキストと呼ぶそうです)は、どのストラテジを利用するかを知らずにすみます。適用前は、ReportクラスはHTMLとPlainText、それぞれの出力方式を知っていました。ただ、Strategyパターンを適用した後は、出力方式に関する知識はReportクラスからストラテジオブジェクトに移りました。 実例 Strategyパターンは、Wardenという認証 フレームワーク で使われています。 github.com Warden::Strategies::Baseのサブクラスに認証の方法を実装することで、その認証方法を切り替えることができます。 その Warden を使った、 Devise というgem(ログイン方法を簡単に実装できる)の ソースコード を見ると、実際にどのようにStrategyパターンが使われているのかを確認できます。 下の ソースコード を見るとわかるように認証するオブジェクトはwardenですが、認証方法に関する知識は DatabaseAuthenticatableクラス と Rememberable クラスが持っています。 module Devise module Strategies class DatabaseAuthenticatable < Authenticatable def authenticate! # データベースにあるデータで認証を行う処理 end end end end module Devise module Strategies class Rememberable < Authenticatable def authenticate! # クッキーを使って認証を行う処理 end DatabaseAuthenticatableは、データベースにあるメールアドレスとパスワードで認証します。Rememberableは、クッキーからデータベースにあるレコードを探してきて認証します。 Observerパターン Observerパターンとは あるオブジェクトの状態の変化を、そのオブジェクト自身がその変化を知りたいオブジェクトに対して知らせる仕組みがObserverパターンです。 システムの各部分が、あるオブジェクトの状態を知りたいとき、例えば、誰かの給料が変わった時に、その変更を 経理 部門が知る必要がある時を想定します。 悪いパターンのサンプルコードを見ていきます。 class Payroll def update (changed_employee) puts( "#{ changed_employee.name } の給料が #{ changed_employee.salary } に変わりました " ) end end class Employee attr_reader :name , :employee def initialize (name, title, salary, payroll) @name = name @title = title @salary = salary @payroll = payroll end def salary= (new_salary) @salary = new_salary @payroll .update( self ) end end payroll = Payroll .new taro = Employee .new( ' Taro ' , ' President ' , 200 , payroll) taro.salary = 300 この ソースコード の悪い点は以下になります。 他のオブジェクトが、財務情報を知る必要が出た時に、実際に変化したのは、Employeeではなく他のオブジェクトなのにも関わらず、Employeeクラスを修正する必要がある。 Rubyによるデザインパターン に書かれていた設計原則として、 変わるものを変わらなものから分離する と述べられています。この原則を当てはめて、変わるもの(誰がtaroの財務状況を知る必要があるか)を変わらないもの(Employeeクラス)から分離させます。 Observerパターンを当てはめると以下のようになります。 class Payroll def update (changed_employee) puts( "#{ changed_employee.name } の給料が #{ changed_employee.salary } に変わりました " ) end end class TaxMan def update (changed_employee) puts( "#{ changed_employee.name } に新しい請求書を送ります " ) end end class Employee attr_reader :name , :employee include Subject def initialize (name, title, salary) super () @name = name @title = title @salary = salary end def salary= (new_salary) @salary = new_salary notify_observers end end module Subject def initialize @observers = [] end def add_observer (observer) @observers << observer end def delete_observer (observer) @observers .delete(observer) end def notify_observers @observers .each do | observer | observer.update( self ) end end end payroll = Payroll .new taxman = Taxman .new taro = Employee .new( ' Taro ' , ' President ' , 200 ) taro.add_observers(payroll) taro.add_observers(taxman) fred.salary = 300 このように、ニュースを持っているオブジェクトとそれを受け取る側にきれいなインターフェースを作るア イデア がObserverパターンです。ニュースを持っているオブジェクトはSubjectと呼ばれ、それに関心のあるオブジェクトはオブザーバーです。今の状態だと、Employeeはどれだけのオブザーバーが自分の給料の変更に関心があるかを知らなくて済みます。なので、新しくtaroの給料の変更を知る必要があるオブジェクトが出てきた場合は、そのオブジェクトがオブザーバーとしての共通のインターフェースであるupdateメソッドを実装して、add_observersするだけで、Employee自身は何も変化しません。 実例 こちらは実際に使われている例ではなく、 rails-observers を使うと簡単に ActiveRecord 用のobserverクラスを作ることができるという説明をします。 github のREADMEのサンプルコードですが、 class CommentObserver < ActiveRecord :: Observer def after_save (comment) Notifications .comment( " admin@do.com " , " New comment was posted " , comment).deliver end end この場合、 Comment#save が終了した際に、after_save内の処理を実行します。 サンプルコードでは、Subjectのセッターに、オブジェクトが変更した際に実行するメソッドを追加する必要がありましたが、 rails-observers を使えば必要ありません。 Iterator パターン Iterator パターンとは Iterator パターンとは集約オブジェクトが子オブジェクトのコレクションにアクセスする方法を外部に提供するテクニックです。 外部 イテレータ ーと内部 イテレータ があります。 外部 イテレータ ーについて、サンプルコードを使って説明します。 class ArrayIteratoor def initialize (array) @array = array @index = 0 end def has_next? @index < @array .length end def item @array [ @index ] end def next_item value = @array [ @index ] @index += 1 value end end array = [ ' red ' , ' green ' , ' blue ' ] i = ArrayIterator .new(array) while i.has_next? puts( " item: #{ i.next_item }" ) end => item : red item : green item : blue 外部 イテレータ ーの場合は上のように、子オブジェクトに対して処理内容を伝えます。 一方、内部 イテレータ ーとは、eachのように、 イテレータ ー自身がブロックを受け取って、 その処理内容を子オブジェクトに伝えます。 実例 内部 イテレータ ーが使われている箇所を、 rails の ソースコード 内の actionpack/lib/action_dispatch/http/mime_type.rb から簡単に探すことができました。 module Mime class Mimes include Enumerable def initialize @mimes = [] @symbols = nil end def each @mimes .each { | x | yield x } end def << (type) @mimes << type @symbols = nil end def delete_if @mimes .delete_if { | x | yield x }.tap { @symbols = nil } end def symbols @symbols ||= map(& :to_sym ) end end SET = Mimes .new class Type def register # 他の処理 SET << new_mime # 他の処理 end 上の例の場合、Mimesクラスの子オブジェクトに対して処理内容を伝える場合は、コードブロックを利用します。 また、Enumberableモジュールをincludeしてeachメソッドを定義することで、 include? , map , select などの配列を走査する際に便利なメソッドを使うことができます。 actionpack/lib/action_dispatch/http/mime_types.rb に下のような処理がありました。 Mime :: Type .register " text/html " , :html , %w( application/xhtml+xml ) , %w( xhtml ) Mime :: Type .register " text/plain " , :text , [], %w( txt ) Mime :: Type .register " text/javascript " , :js , %w( application/javascript application/x-javascript ) Mime :: Type .register " text/css " , :css Mime :: Type .register " text/calendar " , :ics Mime :: Type .register " text/csv " , :csv Mime :: Type .register " text/vcard " , :vcf Mime :: Type .register " text/vtt " , :vtt , %w( vtt ) # 他のMimeタイプをMimeクラスに追加する処理が続く Typeクラスのregisterメソッド内でMimesクラスに追加をする処理があり、 actionpack /lib/action_dispatch/http/ mime _types.rbで Mime タイプを追加していました。 Builderパターン Builderパターンとは オブジェクトの構築のロジックに対して責任をもつBuilderを作り、そのBuilderを使ってオブジェクトを作成するパターンです。 例としては、以下になります。 class Computer attr_reader :drives def initialize (drives=[]) @drives = drives end end class Drive attr_reader :type attr_reader :size attr_reader :writable def initialize (type, size, writable) @type = type @size = size @writable = writable end end class ComputerBuilder attr_reader :computer def initialize @computer = Computer .new end def turbo (has_turbo_cpu = true ) @couputer .motherboard_cpu = TurboCpu .new end def add_cd (writer = false ) @computer .drives << Drive .new( :cd , 760 , writer) end def add_dvd (writer = false ) @computer .drives << Drive .new( :dvd , 4000 , writer) end def add_hard_disk (size_in_mb) @computer .drives << Drive .new( :hard_disk , :size_in_bm , true ) end end builder = ComputerBuilder .new builder.add_cd( true ) builder.add_dvd builder.add_hard_disk( 1000000 ) computer = builder.computer 上の場合は、Computerを作るために必要なcdのsizeが760であるというような、Computerクラスの インスタンス を作成するためのロジックが、ComputerBuilderのクラスに集まっています。Computerを作るための実装の詳細をBuilderクラスが隠蔽しています。 実例 Builderパターンは Builder でグレップすれば良いので、簡単に見つけることができました。 以下の ソースコード は、 mastodon という短文投稿型の SNS サイトのソースです。 github.com # app/lib/rss_builder.rb class RSSBuilder def initialize @document = Ox :: Document .new( version : ' 1.0 ' ) @channel = Ox :: Element .new( ' channel ' ) @document << (rss << @channel ) end def title (str) @channel << ( Ox :: Element .new( ' title ' ) << str) self end def link (str) @channel << ( Ox :: Element .new( ' link ' ) << str) self end ## 他のメソッドが続く RSS とは、Webサイトのニュースやブログなどの、更新情報の日付やタイトル、その内容の要約などを配信するため技術で、 XML 形式で記述されます。 このRSSBuilderは、titleがchannelの子要素であるなどの XML の構成の詳細を隠蔽しています。 この RSS は app/serializers/rss/account_serializer.rb 上でRSSBuilderクラスを使って作成されています。 class RSS :: AccountSerializer def render (account, statuses, tag) builder.title( " #タイトルの名前 " ) .link( " #タグのurl " ) #他のRSSを作成する処理が続く builder.to_xml end このAccountSerializerクラスは RSS の xml の構成の詳細を知りません。タイトルやlinkの情報を与えるだけで RSS を完成させることができます。 まとめ Rubyによるデザインパターン を読んでみて、実際の使用例を探してみようと意気込んでみたものの中々見つけることができませんでした。 普段から、 ソースコード を読む際に デザインパターン を意識して読んでみるのもアリなのかなと思いました。 また、実際に使用例を見つけるための ソースコード リーディングを通して、 デザインパターン に対する理解を深めることができただけでなく、雑多に新たな知識を得ることができました。 そして、モヤモヤしていたことをはっきり理解できた時の快感が ソースコード リーディングの楽しみだなと改めて感じたので、積極的に読んでいこうと思いました。 最後まで読んで頂き誠にありがとうございました。 参考 Design Patterns in Ruby: Strategy Pattern - Ruby Inside - Medium GitHub - rails/rails-observers: Rails observer (removed from core in Rails 4.0) GitHub - rails/rails: Ruby on Rails https://github.com/tootsuite/mastodon 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは!サーバーサイドエンジニアの @hokita222 です! 有酸素運動 は脳を活性化させると聞いて、最近は朝会社に出社せずにランニングしております! それはさておき、これは Enigmo Advent Calendar 2019 23日目の記事です! 今回は弊社が運営するサイトの BUYMA ( Ruby on Rails )に追加した機能で、 STI 、 ポリモーフィック関連 を使ってみたので、どういう設計にしたかを書いていこうと思います。 ※使ってみたって話で、それぞれどういう特徴なのかなどの詳しい説明はしておりません。 どんな機能作ったの? 「〇〇キャンペーン」などの施策で、その日あった取引の中で特定の条件(商品ID、カテゴリーID、何円以上など)のものを絞り込み、その対象の取引に対して特定のアクションをさせます。 今回はこの機能の「特定の条件で絞る」の設計を説明していきたいと思います。 設計するなかで実現したかったこと 「特定の条件で絞る」機能を作るなかで重要視していたのは、「条件が増える可能性が高い」ことです。今は商品ID、カテゴリーIDで絞れるけど、将来的にはブランドID、購入数などで絞れるようにするかもしれません。なので条件が容易に追加できることを意識しました。 テーブル設計 テーブル名 説明 promotions 施策テーブル(施策名、施策期間など) rules 施策の条件テーブル(どの取引を対象にするかの条件) actions 施策のアクションテーブル(対象の取引に対しての行うアクション)※今回こちらは割愛 rule_targets 施策の条件テーブルとターゲット(itemsやcategoriesなど)との中間テーブル items 商品テーブル(元々あるテーブル) categories カテゴリーテーブル(元々あるテーブル) ※promotions, rules, actionsの形は solidus を参考にさせてもらいました。 STI rulesテーブル(ruleモデル)に対して item , category , price_gte というサブタイプクラスを作成しました。 なぜ STI か STI だと条件が増えたときにクラスの追加のみで済む。 具象テーブル継承、クラステーブル継承だとテーブルまで作らないといけないので面倒くさい。 将来的に追加されるカラムが少ない。 あるサブタイプクラスしか使わないカラムが増えていくと、テーブルにカラムが増えすぎるってこともあると思いますが、今回は問題ないと判断。 サブタイプクラスで共通のメソッドでも、それぞれ処理が異なる。 処理が同じなら親クラスで共通のメソッド生やせばすむし、結果 enum を使って異なる処理だけを例外的に書けばいいよねってなりますが、今回はそれぞれ異なります。 Rails の STI の機能は使わなかった Rails 標準の STI の機能は使いませんでした。理由としては「継承」を使いたくないから。今回は委譲で対応しております。( Rails の STI を期待されてた方すみません。) ※と言ってみたは良いものの、 Rails から外れるツラミも結構大きいです。今回の機能では問題ないのですが、あまりおすすめはしないです。 ソースコード # 施策(調整予約)ルールテーブル # # カラム # - type: どの条件か(どの条件のクラスを使用するか。例: item, category, price_gte) # - value: 条件に必要な任意の値(〇〇円以上とか。商品IDとかはここには入らない。) # class Rule < ActiveRecord :: Base self .inheritance_column = nil belongs_to :promotion has_many :rule_targets delegate :build_relation to : :sub_rule private # サブタイプクラス def sub_rule " Rules:: #{ type.classify }" .constantize .new( self ) end end 継承を使用しないので、サブタイプクラスを呼ぶメソッドを作ってます。 module Subtypeable extend ActiveSupport :: Concern attr_accessor :rule delegate :value , :rule_targets , to : :@rule def initialize (rule) @rule = rule end # 特定のrelationをactiverecordのメソッドを使用して絞り込む def build_relation (relation) relation end end ruby ではダックタイピングできるので基本インターフェースは不要ですが、他のエンジニアにもこのメソッド使ってくれという願いを込めてmodule作りました。(ちょっとインターフェース以上のこと書いているのはご愛嬌) # 商品ルール class Syohin include Subtypeable # override def build_relation (relation) targetable_ids = rule_targets.pluck( :targetable_id ) relation.where( ' trades.item_id in (?) ' , targetable_ids) end end 引数の relation に取引のRelationを渡すことによって中間テーブルにある対象のIDたちで絞ることができます。(正確にはRelationに条件を付加します。) # 価格(以上)ルール class PriceGte include Subtypeable # override def build_relation (relation) relation.where( ' trades.price >= ? ' , value) end end こちらは商品ルールとは異なり、rulesテーブルの value カラムを使用します。 Polymorphic関連 なぜPolymorphic関連か rule_targets モデルに対して、複数のモデルを紐付けたかったため。 rule_targets からは items も categories も targets という同類の関係 rules と targets は多対多なので中間テーブル rule_targets とのPolymorphic関連 ソースコード # 中間テーブル class RuleTarget < ActiveRecord :: Base belongs_to :rule belongs_to :targetable , polymorphic : true end class Item < ActiveRecord :: Base has_many :rule_targets , as : :targetable end class Cateogry < ActiveRecord :: Base has_many :rule_targets , as : :targetable end ※本当は STI と同じくインターフェース用のmodule作ったほうがいいのですが、共通のメソッドがないので作ってません。 ちなみに promotionモデルはこうなっております。 # 施策テーブル class Promotion < ActiveRecord :: Base has_many :rules has_many :actions # いろんな条件をまとめたrelationを受け取ることができる。 def detect (relation) rules.inject(relation) do | rel , rule | rule.build_relation(rel) end end end Promotionモデルが窓口となっており、コントローラーなどの呼び出し元は detect メソッドを呼ぶだけで条件での絞り込みが可能になります。 各クラスがそれぞれの仕事を担ってくれているので結構シンプルなのではないでしょうか。また STI 、Polymorphic関連で実装したおかげで if 文, case 文が全くありません。 さいごに 現状新しい施策条件が増えた場合でもクラス一つ作るだけで解決するので、小一時間あれば条件の追加が可能となりました。 明日は弊社新卒の平井くんです!どんな記事書くんでしょうね。楽しみ!わくわく 弊社では一緒にレガシーコード脱却を目指すエンジニア大募集中です! hrmos.co
アバター
こんにちは。Engimo インフラチームの夏目です。 この記事は Enigmo Advent Calendar 2019 の22日目の記事です。 最近は こちらのインタビュー でも触れたとおり Kubernetes クラスタ を作ったり壊したりしていまして、今日の記事は Kubernetes におけるアプリケーションデプロイに関してのお話です。 Kubernetes の継続的デリバリ、どうしてますか? Kubernetes をプロダクション環境で利用されているそこのあなた!アプリケーションをどうやってデプロイしていますか? ローカルでDockerImageをビルド DockerHubのプライベー トリポジ トリへプッシュ kubectl edit でDeploymentsのイメージタグを最新のものへ変更 といった人の手による温かみのあるデプロイをしている? それはそれで心がこもった良いやり方かもしれませんが、おそらく少数派ですし、システムに心は必要ありませんのでやめましょう。みなさん基本的には Spinnaker や JenkinsX といったCDツールを利用されているかと思います。 現在私達が開発中のシステムにおいては、デプロイパイプラインのCDツールとして ArgoCD を利用しています。 以下のような特徴があり、シンプルに GitOps デプロイを実現することができるツールです。 GUI や CLI でmanifestの適用状態が確認できる kustomize に対応している Sync/Hook機能でデプロイフローを柔軟にコン トロール できる そもそもGitOpsとはどういった概念なのか、という点に関しては提唱元であるWeaveworksのblogがわかりやすいのでご参照ください。 www.weave.works なお、Weaveworksが開発している Flux もGitOpsを実現するためのCDツールなのですが、ArgoCDと比べると公式ドキュメントがややわかりづらい印象があります。 また、kustomizeには対応しているものの、他の機能(イメージタグ更新検知)とあわせて利用しようとするとmanifestの構成が複雑になる(選定時の実装では)、といった理由もあって採用には至りませんでした。 EnigmoにおけるArgoCDを使ったGitOpsデプロイフロー ArgoCDの アーキテクチャ は公式ドキュメント記載の以下の図のように、GitOpsの全ての領域をカバーするわけではなく、他のCIツールと併用することを前提としています。 ArgoCD architecture CIツールについては図のようにTravisCIやCircleCI、最近であれば Github Actionsなども候補になるかと思いますが、EnigmoではメインのGitサービスとして GitLab で ソースコード を管理しており、GitLabにビルトインされたCI機能である GitLabCI をフローに組み込みました。 GitLabCIとArgoCDを組み合わせたデプロイフローは以下のような構成です。 GitOps DeployFlow ArgoCDの提唱するBestPractice に則り、アプリケーションのコード リポジトリ と Kubernetes のmanifest リポジトリ は分離させています。 以下にフェーズごとの具体的な流れを書いてみます。 Application Image Build Phase アプリケーション リポジトリ の master ブランチにDockerイメージのバージョンアップMRをマージすると、GitLabCIで以下のジョブが実行されます。 Dockerイメージのビルド・タグ設定 ECRへDockerイメージをPush これでアプリケーション側のデプロイ準備は終わりました。 Application/ Kubernetes Config Deployment Phase 更新したイメージタグを利用するようmanifestファイルを修正し、manifest リポジトリ にMRを作成→マージすると、以下のフローで処理が走ります。 GitLabのmanifest リポジトリ をCodeCommitRepositoryへ ミラーリング (参考: GitLab Repository Mirroring ) Kubernetes 上で稼働しているArgoCDがCodeCommitRepositoryを参照し、 クラスタ の状態とmanifestファイル定義を比較 イメージタグが クラスタ とmanifestで異なることをArgoCDが検知して、 クラスタ のDeploymentsを更新 このように、アプリケーションと Kubernetes manifestのコードをそれぞれGitにマージするだけで、kubectlを使うことなくデプロイが実行されました。 なお、ArgoCDの チュートリアル や概要的な部分は公式ドキュメントにわかりやすくまとめられているため省略させて頂きます。 argoproj.github.io ArgoCDを使ったデプロイフローのPros/Cons Pros デプロイ作業の省力化 システム開発 の初期は冒頭で記載したような、manifestファイルを修正→ kubectl diff で クラスタ とmanifestの差分を確認→ kubectl apply で クラスタ へ適用!という原始時代のようなオペレーションを行っていました。有り得ませんね。 継続的デリバリという方式なので当たり前ではありますが、このフローを構築してからはGitのマージ操作のみでデプロイ作業が完結し、タイトルのようにおよそ5分でアプリケーションがデプロイされるようになりました。 柔軟なデプロイフローの実装 開発中のシステムは Rails アプリケーションで構成されているのですが、 Rails アプリケーションにつきものの db:migrate をどうやって実施するか?という点についても、ArgoCDのSync Waveで解決することができました。 db:migrate を実行する Job を作成し、 annotationで他のDeploymentよりも先にSyncが実行されるように設定 することで、他のアプリケーションよりも先に確実に db:migrate が実行され、 スキーマ 関連のエラーが回避できます。 また、現時点では上記デプロイフローには実装していませんが、 Hookの仕組み を使うことでデプロイ後にSlackへ通知を飛ばしたり、正常終了したJobを削除する、といった振る舞いを加えることもできます。 Cons デプロイスピード これはArgoCDの短所ではないのですが、このデプロイフローは構成図でもわかるようにオンプレミスのGitLabと、 AWS のマネージドサービス群の2つにわかれています。 本来であればArgoCDから直接GitLabのmanifest リポジトリ を参照し、GitLabのContainerRegistryからDockerImageをPullするようにすれば、オンプレミスからのイメージPushやRepository ミラーリング が必要なくなり、もっとシンプルなフローになります。 ただ、 AWS 側からオンプレミスのGitLabへアクセスするよう設定すると、オンプレミス側への依存が発生することと、 Kubernetes クラスタ からはあくまで AWS のサービスのみとやりとりをするのが、リソースアクセス権限設定の観点からもわかりやすいだろう、という理由からこういった構成になっています。 なお、manifest リポジトリ の ミラーリング はリアルタイムではなくおよそ5分間隔で実行されます。これに加えて、ArgoCDが クラスタ とmanifestの差分を 3分間隔(調整可能) でチェックしているため、マージしてから最長8分待たないとデプロイされない、という状態になっています。 いざデプロイしようと思ってマージをしてから実際にアプリケーションが更新されるまでの時間がやはり長過ぎるのではないか、という印象は否めないため、前述した理屈を否定する形にはなりますが、GitLabを AWS へ移行してArgoCDから直接参照させる、といった方法の改善を検討しています。 イメージタグ更新修正の手間 デプロイフローに記載したように、アプリケーション リポジトリ の更新だけではなく、manifestファイルのイメージタグも都度修正しなければなりません。アプリケーションのコードを更新したら即デプロイ!ということにはならず、もうワンアクションが必要になります。 プロダクション環境であれば、意図しないデプロイを防ぐためにもそういったアクションが挟まるのも良いかもしれませんが、テスト環境のようにスピーディーにデプロイして検証をしたい!という場合は手間でしかありませんね。 ArgoCDの競合プロダクトであるFluxはそういった手間が省略できる、イメージタグの自動検知・適用機能があるため、 同じような機能がほしい!というIssue がArgoCDの リポジトリ に報告されていました。 FluxとArgoCDは設計思想が違うから、というコメントとともにあえなくクローズされる……かと思いきや、ArgoCDとFluxがそれぞれの機能を統合したGitOpsツールの開発をすすめる旨の発表をしたため、緩やかに期待してArgoCDを使い続けよう、という状況です。 www.weave.works まとめ Kubernetes のGitOpsツールとしてArgoCDを利用しています Dockerイメージのビルドとプッシュ用CIツールとして、GitLabCIを利用しています ArgoCDを利用することで柔軟なデプロイフローを実現することできます この記事では我々のシステムではどのようにGitOpsデプロイを実現しているか?という構成を紹介させて頂きました。 Kubernetes の運用ツールはまだまだ デファクト と呼べるようなものが揃っていない状況ですが、日々新しい機能が盛り込まれたり統廃合される様子は、ウォッチしていて楽しいものでもありますね。 Enigmoでは Kubernetes をはじめとしたCloud Native Computingの運用に一緒に立ち向かう仲間を募集しています。 hrmos.co 明日の記事の担当はサーバサイドエンジニアの @hokita222 さんです。おたのしみに。
アバター
こんにちは。サーバサイドエンジニアの伊藤です。 Enigmo Advent Calendar 2019 、21日目の記事です。 先週末の12月14日(土)、 平成Ruby会議01 に登壇し、「Play with Ruby 」という題で発表してきました。 タイトルからはわかりにくいのですが、 parse.y をライブコーディングで操作し、雑な感じに右代入を実装するという話です。 TL;DR 内容 発表に至るまで 平成Ruby会議01 当日 トークセッション 発表 懇親会 最後に TL;DR とっても楽しかったです。 内容 RubyKaigi2019 で聴いた 「Play with local vars」by Tatsuhiro Ujihisa/@ujm の トーク に影響を受けて、右代入を実装した話です。 詳しくはこちらのスライドを御覧ください。 speakerdeck.com 発表に至るまで 実は今回が初めての人前での発表でした。 2019 年中に人前で発表することが1つの目標だったので、とりあえず CfP *1 を出してみました。 CfP を出した時からずーっと気が休まる暇がなかったように思います。CfP が採択されるまでは心のどこかで却下されることを祈っていましたし、採択されてからは発表当日インフルエンザにでもなってくれないかと思っていたほどです。。。 結果的には有難いことに採択され、幸運なことにインフルエンザにもならず、2019 年中に人前で発表するという目標を達成することができました。 ちなみに、CfP を提出した時点では parse.y をまともに読んだこともありませんでした。右代入の具体的な実装などは皆目見当がついておりませんでした。完全な勢いです。。。 CfP Advent Calendar 2019 というものを発見しましたので、 CfP の内容はこちらに公開してあります。 sean0628.hatenablog.com こちらを見ていただけるとわかると思うのですが、右代入を実装することはこの時点で決めていました。 ただ、右代入の実装方法が思いつかない最悪のケースが頭をよぎり、チキリにチキリきった結果、発表のタイトルを「Play with right-assignment」ではなく「Play with Ruby 」としてしまいました。 わかりにくいタイトルになってしまいすみませんでした。。。 平成 Ruby 会議01 当日 トーク セッション いろいろな角度からの発表があり、とても刺激的でした。正直、自分の発表のことで頭がいっぱいで、全体の半分くらいしか発表を見ることはできませんでした。。。 聴講だけで、もう一度参加したいくらいです。 個人的に1番印象に残っているのは、金子さんのキーノート 「What is expected?」 です。 内容はパーサーの話でした。ちょうど1ヶ月前に聞きたかったです。かなり深いところまで解説されていて、理解できなかったところもありました。時間を見つけて復習したいと思います。 発表 さて、自分の発表ですがみなさまのおかげで全体的にはスムーズに進めることができたかなと思っております。 今回初めてライブコーディングにも挑戦しました。 スライドのメモ確認のために敢えて ミラーリング してなかったのですが、スクリーンの角度が急すぎてエディターが何も見えないというハプニングが発生しました。 さらには、ライブコーディング中の1番緊張していたタイミングで、 Siri が起動するという奇跡的なトラブルにも見舞われました。 (今年1番 、いや人生の中で1番 Siri に怒りを覚えました。) そんなトラブルもありましたが、発表を聴きに来てくださった方々が暖かく見守ってくださったお陰でなんとか最後まで辿り着くことができました。 ありがとうございました。 懇親会 懇親会では Ruby や Rails のコミッタさんやコントリビュータさんから、 OSS との関わり方や RubyKaigi の裏話など貴重なお話を伺うことができました。 また、キーノートスピーカーの金子さんからは、右代入の実装に関して具体的なアド バイス をいただきました。 今回いただいたアド バイス をもとに、また Play したいと思います。 最後に 運営・スポンサー・参加者の皆さん、ありがとうございました。 みなさまのおかげで素敵な1日を過ごすことができました。 株式会社 エニグモ 正社員の求人一覧 hrmos.co *1 : Call for Proposal
アバター
こんにちは! 冬が苦手なディレクターの神吉です。 この記事は Enigmo Advent Calendar 2019 の 20日 目の記事です。 LINEの開発者情報をチェックしていて、ちょっと前にLIFF v2がリリースされていました。 https://developers.line.biz/ja/docs/liff/release-notes/#spy-releasedate_20191016 LIFFとは LIFFとはLINE Front-end Frameworkのことで、LINEが提供するウェブアプリのプラットフォームです。 JavaScript を書いて開発する感じです。 LIFFはけっこう前から提供されていましたが LIFF v1 → LIFF v2になって主に下記ができるようになりました。 外部ブラウザでLIFFアプリが動作する。 v1はLINE内ブラウザでのみ動作していました。 ユーザーのプロフィール情報とメールアドレスを取得できる。 LINEログインでできる仕組みに近い仕組みなのかなと思います。 QRコード を読み取れる。 新機能ですね! LIFFアプリの動作環境を細かく取得できる。 外部ブラウザで動作するようになって細かい情報を取得する必要がでてきたのかと思います。 LIFF v1もちゃんと触っていなかったのですがこの機会にv2を触ってみたいと思います。 とりあえずLIFFで QRコード を読み取りをやってみます。 事前準備 LINE ログイン を利用できるLINEアカウント ※Messaging API チャネルではないほうが良いです。 https://developers.line.biz/ja/docs/liff/release-notes/#spy-releasedate_20191111 HTTPS URL対応サーバ こちらを事前に準備してください。 LIFFアプリの追加 LINE Developersコンソールよりの 「LIFFアプリを追加」より必要事項を入力してLIFFアプリの追加をしてください。 ※今回は QRコード を読み取りをするのでScan QR の設定はONにしてください。 またLIFFサーバー API でもLIFFアプリを追加することができます。 https://developers.line.biz/ja/reference/liff-v1/#add-liff-app 追加後LIFF URLが発行され、LIFFを開くことができるようになります。 LIFFアプリの開発 ここからは実際の開発手順です。 LIFF SDK を読み込み LIFF SDK https://static.line-scdn.net/liff/edge/2.1/sdk.js を読み込んでください。 <!DOCTYPE html> < html lang = "ja" > < head > < meta charset = "UTF-8" > < title > sample </ title > </ head > < body > < script src = "https://static.line-scdn.net/liff/edge/2.1/sdk.js" ></ script > </ body > </ html > LIFFアプリを初期化する sdk.js 読み込み後に追加してください。 liff .init( { liffId: "XXXXX" // liffIdを指定。line://app/XXXXXの部分 } ) .then(() => { // 処理はここから } ) . catch ((err) => { // エラー処理 console.log(err.code, err.message); } ); QRコード を読み取り処理 document .getElementById( 'qr_button' ).addEventListener( 'click' , function () { if (liff.isInClient()) { liff.scanCode().then(result => { document .getElementById( 'qr_text' ).textContent = result.value; } ). catch (err => { document .getElementById( 'qr_text' ).textContent = err.message; } ); } } ); liff.scanCode() はLINEの QRコード リーダーを起動し、読み取った文字列を取得するメソッドです。 #qr_button にクリックするとLINEの QRコード リーダーが立ち上がり、 QRコード をスキャンすると #qr_text に QRコード で読み取ったテキストが挿入されます。 注意点 iOS 版LINEバージョン9.19.0以降では、 liff.scanCode() の提供を一時停止しています。(2019/12/18時点) liff.scanCode() は、外部ブラウザでは利用できません。 どう活用する? 手軽に QRコード リーダーを使えるのは良いです。 QR の読み取りも速いです。 またLIFFはLINEのuserIdと紐づけてイベントログを取得でき、データ活用できます。 そういったところ活かしオンラインとオフラインをつなぐような施策使えればと思います。 (今のところ BUYMA での具体的な活用案が浮かばずすみません。。) 感想 かなり簡単に試すことができました! LIFFを使用する上で実行環境による挙動の違いはしっかり把握しないといけないなと感じました。 今後もLINEの開発者情報はこまめにチェックして、新機能は早めに試してみたいと思います。 参考 LINE Front-end Framework LIFF v2 APIリファレンス 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
インフラチームの山口です。 ゼロ年代 後半ゆるふわ情報系学生でしたが紆余曲折の末にインフラエンジニア1年目となりました。 今回は編集距離を使用して SQL のクエリを クラスタリング してみたので記事にまとめてみます。 奇しくも、 伊藤直也 さんのブログで編集距離の記事が公開されたのが2009年だったのですが、時の流れの速さを感じてしまいます。 1.背景 DBのCPU負荷のスパイク時に、DBのクエリのログを取得・人手で集計して、CPU負荷が高いクエリを改善するという運用を実施することがあります。 ログ(クエリ)の量が少ない場合は良いのですが、大きくなるにつれ、人手での集計に伴い以下のような問題が発生しています。 人手での集計には時間を要する 作業者が変わると結果が一意に決定できない場合があり、集計作業の再現性がない スクリプト に起こして作業をしようとしても、 単純な文字列一致の方法で集計を試みると、 WHERE の条件にしている id が異なっているだけといったクエリをうまく拾えないことが予想されます。 今回は、2つの文字列に対して定義される編集距離(Levenshtein Distance)を使用して、 スクリプト での結果の集計を試みます。 (「TF-IDFやNグラムを使用してやるのもちょっと……」という個人的な気持ちがあったので) 2.関連する事例 「編集距離を使用してクエリ同士の距離を測ればなんかうまくいきそうな気がする」というワンアイディアで調べもせず スクリプト の作成をしてしまったのですが既存の参考文献を調べてみます。 sql query clustering filetype:pdf などで検索してみるといくつか事例が見つかります。 詳しい書誌情報は割愛でタイトルとざっと読んでどんな事やっているかだけ記載します。 クエリの クラスタリング というタスクの論文は書かれているようです。 論文書かれているということは、おそらく他のエンジニアも同じ問題にあたった人もいるはず。 (手でクエリのログ分類して大変な思いしてるのは私だけではなさそう……。) "Similarity Metrics for SQL Query Clustering", 2018. 先行研究で提案された特徴量がまとめられている。 著者らの提案手法については、理解する時間がなかったが、クエリの クラスタリング のタスク自体の概要を知るには良さそう。 "Text Mining Applied to SQL Queries: A Case Study for the SDSS SkyServer", 2015. ↑ の文献で引用されていたもの。 クエリのTF-IDFを特徴量にしてSOMで クラスタリング 、可視化をしている。 3.方法 編集距離とは( Wikipedia 参照) 編集距離については既存で参考になる記事が複数存在するため、本記事では特に説明せず、 Wikipedia からガッツリ引用します。 端的には2つの文字列の間の距離の尺度です。 レーベンシュタイン距離(レーベンシュタインきょり、英: Levenshtein distance)は、二つの文字列がどの程度異なっているかを示す距離の一種である。 編集距離(へんしゅうきょり、英: edit distance)とも呼ばれる。 具体的には、1文字の挿入・削除・置換によって、一方の文字列をもう一方の文字列に変形するのに必要な手順の最小回数として定義される編集距離(Levenshtein distance)は2つの文字列がどの程度異なっているかを示す距離の一種。 例 kitten を sitting に変更する場合は、最低でも3回の手順が必要とされるので2単語間の編集距離は3となる。 kitten sitten(kをsに置換) sittin(eをiに置換) sitting(gを挿入して終了) 今回の方法 以下の要件を満たす、なんちゃって クラスタリング を実行する スクリプト を作成しました。 クラスタリング 結果が一意に決まらない方法は避けたい 手元のマシンですぐに結果がみたい(時間がかかる方法は避けたい) 今回の方法を説明します。 クラスタ の代表と以下で記載していますが、実際はレコード中で一番はじめに出てきたものをその クラスタ の代表にしているだけで、特に重心などをとっているわけではないです。 また、今回は、クエリの先頭からN単語分を抜き出して使用します。 これは、「人間が比較する際もクエリの先頭から末尾まで見ておらず、先頭N文字で判断しているだろうという仮定」と、「試しにクエリ全体で計算してみると思いの外時間がかかった」ためです。 今回の方法の概要 - クラスタリング対象のクエリすべてに対して以下を実行する - クラスタリング対象の1つめのクエリの場合はそのクエリをクラスタ0の代表としクラスタ0に追加する - クエリと各クラスタの代表のクエリの先頭からN単語の部分文字列との編集距離を計算 - 編集距離が閾値以下かつ、最も近いクラスタに割り当てる - 閾値以下の距離のクラスタがない場合は新しいクラスタを作成する また、編集距離を取得する際に、一旦クエリをスペースで分割して、単語単位での編集距離を取得します。 日本語だと 形態素解析 が必要ですが、雑に半角スペースで単語に分割します。 >>> import editdistance >>> q1 = 'SELECT * FROM tbl_1 WHERE id = 1;' >>> q2 = 'SELECT * FROM tbl_1 WHERE id = 50;' >>> q3 = 'UPDATE tbl_1 SET id = 40 WHERE id = 1;' >>> editdistance.eval(q1.split( ' ' ),q2.split( ' ' )) 1 >>> editdistance.eval(q1.split( ' ' ),q3.split( ' ' )) 6 >>> import Levenshtein >>> Levenshtein.distance(q1,q2) 2 >>> Levenshtein.distance(q1,q3) 22 パラメータと使用したライブラリおよび スクリプト 使用したライブラリ editdistance (0.5.3) パラメータ(パラメータは エイヤで 経験的に決定した) 同じ クラスタ と判断するための 閾値 : 10 編集距離10以下で一番近い クラスタ に所属させる 編集距離10以下の クラスタ がない場合は新しい クラスタ とする 比較に使用する単語数: 先頭から100単語 SQL の先頭100単語を切り出し編集距離を計算する スクリプト #!/usr/bin/env python3 # coding: utf8 import csv import editdistance from collections import defaultdict # ファイルを読み込む def load_file (in_fname, skip_header= False ): queries = [] with open (in_fname, newline= "" , encoding= "utf8" , errors= 'ignore' ) as f: queries = list (csv.reader(f, delimiter= " \t " , quotechar= '"' )) if skip_header: del queries[ 0 ] return queries # 結果をファイルに書き込む def write_file (out_fname, result): with open (out_fname, 'w' , newline= "" , encoding= "utf8" ) as f: writer = csv.writer(f, delimiter= " \t " ) writer.writerows(result) # 半角スペースでクエリを分割して、先頭word_len単語のlistを作る def get_substr (query, word_len): return query.split( ' ' )[ 0 :word_len] # クエリの編集距離を計算する def calc_distance (query_1, query_2): return editdistance.eval(query_1, query_2) # 編集距離を使ってクエリをグループ化する def cluster (queries, query_pos= 1 , word_len= 100 , threshold= 10 ): clusters = [] result = [] for row in queries: current_query = row[query_pos] current_query_substr = get_substr(current_query, word_len) # 1レコード目の場合は新規のクラスタに入れる if len (clusters) == 0 : clusters.append(current_query_substr) row.insert( 0 , 0 ) result.append(row) continue # 2レコード目以降 current_cluster = "" # 初期値 min_dist = float ( 'inf' ) for cluster_id, cluster_query_substr in enumerate (clusters): current_dist = calc_distance(cluster_query_substr, current_query_substr) # 閾値以下 if current_dist <= threshold: # クラスタとの距離が近ければ所属するクラスタと編集距離を更新する if current_dist <= min_dist: min_dist = current_dist current_cluster = cluster_id # 閾値以下のクラスタがなければ新規のクラスタにする if current_cluster == "" : clusters.append(current_query_substr) current_cluster = clusters.index(current_query_substr) row.insert( 0 , current_cluster) result.append(row) return clusters, result # 各クラスタのメンバー数などのサマリを出力 def cluster_summary (clustering_result, cluster_pos= 0 ): cluster_members = defaultdict( int ) for row in clustering_result: cluster_id = row[cluster_pos] cluster_members[cluster_id] += 1 # サマリ print ( "### Clustering Summary ###" ) # レコード数 print ( "#of records {}" .format( len (clustering_result))) # クラスタ数 print ( "#of clusters {}" .format( len (cluster_members.keys()))) # 要素数上位10のクラスタ print ( "### top 10 clusters ###" ) print ( "rank \t cluster_id \t #of member" ) for indx, v in enumerate ( sorted (cluster_members.items(), key= lambda x: x[ 1 ], reverse= True )): print ( "{} \t {} \t {}" .format(indx+ 1 , v[ 0 ], v[ 1 ])) if indx >= 9 : break # 各クラスタのメンバー数 print ( "### #of member in clusters ###" ) for c_id in cluster_members.keys(): print ( "{} \t {}" .format(c_id, cluster_members[c_id])) if __name__ == '__main__' : # inputfile base_fname = 'bombay_queries.tsv' base_fname_without_ext = base_fname.split( '.tsv' )[ 0 ] in_fname = "data/{}.tsv" .format(base_fname_without_ext) # parameter word_len = 100 threshold = 10 # outputfile out_fname_result = "result/{}_result_wordlen{}_threshold{}.tsv" .format(base_fname_without_ext, word_len, threshold) queries = load_file(in_fname, skip_header= True ) clusters, clustering_result = cluster(queries, query_pos= 2 , word_len=word_len, threshold=threshold) write_file(out_fname_result, clustering_result) cluster_summary(clustering_result) 4.結果 デー タセット "Similarity Metrics for SQL Query Clustering", 2018. で使用されているのと同じ データセット の中の、 bombay_queries.csv を用いました。 デー タセット には アノテーション されたラベルが付与されています。 id label query 0_1_1 1 select course.course_id,course.title from course 0_1_2 1 select course_id from course 0_1_3 1 select distinct course_id,title from course 0_1_4 1 select course_id,title from course 0_2_1 2 select course_id,title from course where dept_name='comp. sci.' 0_2_2 2 select distinct course.course_id,course.title from course where course.dept_name='comp. sci.' 0_2_3 2 select course.course_id,course.title from course where dept_name='comp. sci.' 0_2_4 2 select course_id,title from course where course.dept_name = 'comp. sci.' 0_2_5 2 select course_id,title from course where course.dept_name='comp. sci.' クラスタリング の結果と所感 クラスタリング 結果を以下に示します。 結果の評価には、 こちら の説明を参照し、PurityとInverse Purityと F値 を使用します。 purity,inverse purity, F値 は0から1の間で1に近づくほど良い値です。 purityが0.75とやや大きいですが、これは クラスタ のメンバー数1の クラスタ が多数あるためです。 参照にした資料でも説明がありますが、1 クラスタ 1レコードに クラスタリング された場合purityは1になります。 今回の結果では、 218 クラスタ 中で クラスタ のメンバー数が1の クラスタ は156個ありました。 結果を定性的に確認してうまくできていそうな箇所を無理やり見つけます。 クラスタ 108の結果を見ると意図したとおりに クラスタリング できているように見えます。 108の結果をよく見ると、同じクエリですがスペースの差異があるもの( count(*)>1 と count(*) > 1 )を同じ クラスタ にまとめられています。ただ、完全文字列一致でやっても、事前に空白除去しておけば拾える箇所なので、編集距離を使った良さとは強く言えなそうです。 また、今回使用したデー タセット はとりあえず、クエリに アノテーション されているものを見つけたので、きちんとした理由もなく使っているのですが、そもそも私が クラスタリング したい目的とデー タセット 作成時の目的があっているのかなども確認する必要がありそうです。クエリの クラスタリング がうまくできると、嬉しいものの、社内で アノテーション するメンバーを複数人集めて、ラベル付きデータ作って評価するほどのものでも無いような気もするしなという気持ちもあります。 レコード数と クラスタ 数 #of records|629 #of clusters| 218 クラスタ のメンバー数降順10 クラスタ rank cluster_id #of member 1 0 127 2 1 33 3 14 31 4 12 22 5 134 16 6 3 14 7 145 14 8 16 13 9 48 10 10 2 8 purity,inverse purity, F値 purity inverse purity F measure 0.75 0.27 0.40 クラスタ 108の結果 cluster_id label query 108 9 select student.id,count(id) from (section join takes using (course_id)) natural join student group by id,name having count(id) = 2 108 9 select id,name from section natural join takes natural join student group by id,semester,year,time_slot_id,name having count(*)>1 108 9 select id,name from section natural join takes natural join student group by id,semester,year,time_slot_id,name having count(*) > 1 5. まとめ 編集距離を使用しクエリの クラスタリング を試みました。 クラスタリング 方法や、パラメータ、テストに使用したデー タセット など、全体的にエイヤな部分が多いですが、定性的に結果をみると類似したクエリをなんとなくまとめることができたようにも思います。実業務でクエリのログの クラスタリング に使えるかはかなり怪しいのと、Datadogなどの SaaS でもログの クラスタリング はできるようなので、ローカルであえて スクリプト 準備して クラスタリング する機会はないのかなとも感じます。 かなり散漫な記事となってしまったので無理やりまとめますが、新しい仕組みがどんどん出てくるなかで、昔からのやり方と新しいやり方適材適所でうまく扱えるようになっていきたいなと思います。 あと、もっと楽に精度良くやれる方法を教えてくれる人がいれば教えてほしいです(かなり切実)。 6. 参考 編集距離 (Levenshtein Distance) レーベンシュタイン距離 python-Levenshtein editdistance jkkummerfeld/text2sql-data クラシックな機械学習の入門 8. クラスタリング 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
サービスエンジニアリング本部の山本です。 この記事は Enigmo Advent Calendar 2019 の 18 日目の記事です。 普段はフロントエンド中心の開発をしていますが、たまに DX(Developer Experience) 的なことにも手を出しています。 今回はそんな DX のお話です。 やばい CI エニグモ が運営している BUYMA は Ruby on Rails アプリケーションとして動いており、自動テスト フレームワーク として RSpec を採用しています。 CIツールとしては Jenkins を採用していましたが、1 年以上の期間、常に Fail しているという エニグモ のようなイケてるウェブ企業としてはあるまじき状態が続いていました。 Jenkins は素晴らしいソフトウェアですが、当時動いていた Jenkins のバージョンは 1 系かつオンプレミスサーバーで動いていたこともあり色々と問題を抱えていました。 また、 エニグモ では Git ホスティング サービスとして Gitlab を採用していて、ちょうど社内では Gitlab CI を使っていこうという流れもあったため RSpec も Gitlab CI に移行することにしました。 Jenkins 時代の課題 ローカルと CI で実行結果が違う ローカルだと通ってたのに CI だと通らない 実行するだけで 2 時間くらいかかっていた 並列実行したいがサーバーのリソースの問題で難しい ブランチごとに実行出来ないため、マージしてみないと CI 上の結果がわからない ブランチごとに実行しようとするとサーバーのリソース不足になる ローカルですべてのスペックを実行するのは非現実的なため関係ありそうな箇所だけローカルでテストしたらマージしてしまう # jenkins という slack チャンネルがありましたが、失敗しているのが当たり前になってしまっていたため誰も気にしておらず、失敗通知が虚しく響き渡っていました。( 割れ窓理論 ) どうやって解決していったか ローカルと CI で実行結果を同じにしたい! まず、ローカルと CI での実行結果が違う問題について。 これは単純に Docker を使うことにしました。 BUYMA の開発環境はもともと Vagrant を使っていて Docker 化はされていませんでした。 今回はローカルの開発環境全部を Docker 化するのではなく、一旦テスト実行環境だけを Docker 化して CI 上でも同じイメージを利用する方針としました。 Gitlab CI にはジョブを実行する Gitlab Runner というツールを利用します。 Gitlab Runner の実行方式である Executor は Shell 、 Docker などの複数のなかから選択することができますが、今回は後述の実行時間短縮を実現するために並列で実行することを考えて Docker を選択しました。 Docker 上で Docker を動かす Docker in Docker という方式があり、それ専用のイメージも用意されているのでそちらを採用しました。 詳しくは こちら 。 各ブランチのMR ( Merge Request )ごとに実行したい! Jenkins 時代は開発用メインブランチである develop ブランチにマージされた時だけテストが走るようになっていたため、 MR をマージして初めてテスト結果がわかるという状態でした。 この場合マージした後に失敗したことに気づき修正する、という流れになりますが、テスト時間が長いこともあり失敗していることに気づかず放置されてしまいます。 マージする前にテストを実行する、という当たり前のことが出来ていない状態だったのです。 これを解決するための Gitlab CI の設定はかんたんで、 .gitlab-ci.yml にこのように記述するだけです。 only : - merge_requests 問題はリソースです。 Gitlab CI への移行当初、 Gitlab Runner サーバー はオンプレミスのサーバーだったため、複数の MR が同時に作成され RSpec のジョブがトリガーされた場合リソースが足りなくなってしまう懸念がありました。 そこで Gitlab Runner サーバー を AWS へ移行して Gitlab Runner の AutoScaling 機能を利用しました。 AutoScaling については こちら の記事が参考になります。 移行前の構成 移行後の構成 この構成の設計や移行作業は全て弊社インフラエンジニアである 夏目さん によるものです。ありがとうございました!! 実行速度を速くしたい! さて、ブランチごとに実行する環境は整いました。 しかし全てのテストが完了するまで2時間ほどかかってしまうという最大の問題が解決されていません。 MR を作成してマージするぞ!っと思ってからテストが終わるまで 2 時間は流石に待ってられません。 RSpec を速くする方法としては、テストコード内で無駄にレコードを作らないなど色々プ ラク ティスがあると思いますが、数百ファイルのテストファイルを全て見て修正するというのは現実的ではありませんでした。 そこで RSpec の並列化という方法で解決することを考えました。 元々 CI 上でのテストは他のテストファイルの影響を無くすため、通常の rspec コマンドではなくそれぞれのテストファイルを独立して実行する スクリプト を利用していました。 #!/bin/bash # 全てのスペックファイルを単独で実行するためのスクリプト。 fail_count = 0 spec_array = () while IFS = read -r -d $' \0 ' spec; do spec_array+ = ( " $spec " ) done < < ( find spec/ -name ' *_spec.rb ' -print0 ) spec_count = ${#spec_array[ @ ]} echo " Running $spec_count spec files " export SKIP_CODE_COVERAGE= 1 for (( i= 0 ; i<$spec_count; i++ )) ; do echo -e " \n ######################################################################### " echo -e " \n Running spec $(( i+ 1 )) of $spec_count " if ! bin/rspec " ${spec_array[$i]} "; then (( fail_count++ )) fi export SKIP_DATABASE_SEEDING= 1 done echo -e " \n $spec_count test files, failures in $fail_count files \n " [[ $fail_count -eq 0 ]] この スクリプト と Gitlab CI の parallel オプション を利用して並列化を実現しました。 まず、 .gitlab-ci.yml のジョブに parallel フィールドを追加します。 今回は 10 個のジョブを並列で実行することにしました。 parallel : 10 この parallel フィールドを追加すると、 ジョブの 環境変数 として CI_NODE_TOTAL と CI_NODE_INDEX という 環境変数 がセットされます。 CI_NODE_TOTAL には parallel で指定した値、 CI_NODE_INDEX にはそのジョブのインデックスがセットされるので、この 2 つの 環境変数 から実行するファイルを算出します。 並列化後の スクリプト はこちらです。 #!/bin/bash # 全てのスペックファイルを単独で実行するためのスクリプト。 fail_count = 0 spec_array = () while IFS = read -r -d $' \0 ' spec; do spec_array+ = ( " $spec " ) done < < ( find spec/ -name ' *_spec.rb ' -print0 ) spec_count = ${#spec_array[ @ ]} echo " CI_NODE_TOTAL is $CI_NODE_TOTAL " echo " CI_NODE_INDEX is $CI_NODE_INDEX " count = $(( spec_count / CI_NODE_TOTAL )) start_index = $(( count * ( CI_NODE_INDEX - 1 ))) if [[ $CI_NODE_INDEX -eq $CI_NODE_TOTAL ]] ; then count = $(( spec_count - start_index )) fi echo " Running $count spec files " echo " Start at $start_index " export SKIP_CODE_COVERAGE= 1 for (( i=$start_index; i<$start_index+$count; i++ )) ; do echo -e " \n ######################################################################### " echo -e " \n Running spec $(( i+ 1 - start_index )) (total: $(($i + 1 )) ) of $count (total: $spec_count ) " if ! bin/rspec " ${spec_array[$i]} "; then (( fail_count++ )) fi export SKIP_DATABASE_SEEDING= 1 done echo -e " \n $count test files, failures in $fail_count files \n " [[ $fail_count -eq 0 ]] テストを 10 分割することにより 2 時間かかっていたテストが 約 20 分 で完了するようになりました! 残念ながらジョブ実行用のスポット インスタンス の起動にある程度時間がかかるため、 10 分割で 10 倍速くなるというわけにはいきませんでした。 また、これ以上並列数を増やしても速くなることもありませんでした。 結果 ローカル環境と CI 環境での差異がなくなったため、ローカルで成功したのに CI では落ちることがなくなった MR ごとに実行できるようになったためテストが成功した状態でマージでき、メインブランチは常にクリアな状態が保てるようになった (たまに落ちてることもある) テストが 20 分で終わるようになった 所感 長らく不満に思っていた、 常に Fail している CI から脱却することができました。 人によるとは思いますが、個人的には CI が落ちて放置されているというような状態に精神が乱されるタイプなので心の平穏を取り戻すことができました。 上記の作業をする前に落ちまくって放置されているテストを全て直すという作業があったのですが正直そこが一番きつかったです。 エニグモ ではカオスな開発環境を一緒に正常化していきたいエンジニアを募集中です! hrmos.co
アバター
はじめに みなさん、こんにちは。 主に BUYMA の検索周り、時々 機械学習 なエンジニアの伊藤です。 今年もあっという間の1年でしたね。 振り返ってみると多くのことを学ばせてもらい、また成長させてもらった1年でした。 その中でも k8s がやはり自分の中では中心だった気がします。 なので今回は k8s ネタで今年を締めくくりたいと思います。 k9sとは? k9sとは k8s 上のリソースを監視、管理するための CLI ツールです。 k8s の CLI といえば kubectl が デファクト ですが、そのI/Fをよりリッチに使いやすくしたのがこのツールです。 公式のページは こちら 読み方は けぃないんず だと思います(たぶん) 個人的になぜ犬のロゴなんだろうというのがまず引っかかったので軽く調べたところ、 canine=犬 のヌメロニム表記であるk9と、 kubernetes の k8s が表記的に近いから犬なんだなと勝手に納得することにしました。 使ってみる では実際にk9sをインストールして使ってみましょう。 Linux 、 OSX 、 Windows 上で動作するようですが、今回は Mac 上でインストールして動かしてみたいと思います。 インストール brew install derailed/k9s/k9s 起動 k9s 表示される情報については大きく2つの領域に分かれており、上部に現在使用しているkubectlのcontext、 クラスタ 名や、コマンドヘルプ等が固定表示され、 枠内にpodの一覧など、指定した情報が表示されるようになっています。 デフォルトでは kubectl get pod で取得できる項目相当の一覧が見れますが、CPU、メモリ使用率なども表示されます。 基本操作について 基本的なところさえ抑えておけば、あとは直感的に操作が可能です。 コマンドモード : でコマンドモードになります。 続けて、 k8s のリソース名(alias名でも可)を指定することでその一覧が表示されます。 指定するリソース名を忘れた場合は Ctrl-a で確認できます。 例えば、deploymentの一覧を確認したい場合は :dp と入力して Enter でdeploymentの一覧が表示できます。 Esc でコマンドモードを抜けます。 フィルター / のあとにフィルターを指定することで絞り込みが可能です。 kubectlと同じうように -l app=XXXX で任意のラベルで絞りこみが可能ですし、 任意の文字列だけ入力してもその文字列がpod名やnode名に含まれれば絞り込みが可能です。 Esc でフィルターを解除します。 ソート Shift と特定のキーで項目毎のソートが可能です 各項目とキーの マッピング は ? で確認できます。 昇順/降順の切り替えは Shift-i で行います。 実践的な使いどころ 基本操作が分かったところで、よりk9sの便利さが伝わるように下記のような場面を想定した操作をしてみます。 あるdeploymentリソースに異常があり、該当のdeployment及びその配下のpodのdescribeを確認して、ログを確認する podへのport-forward 所感 kubectlに慣れている人や、aliasやpeco等でコマンド操作自体をゴリゴリにチューニングしている場合は、 k9sの使い始めは慣れない分、「そこまで便利か?」っていう感じを受けるかもしれません。 自分は最終的にどちらかに移行するというよりは、下記のような使い分けをすることで落ちついています。 単発の確認はkubectl 例えば今のpod一覧を知りたい時など、特定の情報だけ知りたい場合はわざわざk9sを起動する方が手間なので、 kubectl だけで済ませています。 解析作業や複数のコンテキストを跨る作業 こういう場合は圧倒的に k9s が便利です。 上でも記載していますが、 クラスタ 側に何か異常があった場合には、様々なリソースの情報を見ていくことになると思います。 そういった場合にはk9s上であれば、 わざわざコマンドを打つことなく少ないキー操作で確認が可能です。 また、複数のkubectlコンテキスト内のリソースを同時にみるのはkubectlではできませんが、 k9sでは別ターミナルで起動してコンテキストを切り替えることで、 ターミナルの切り替えだけで複数コンテキストの操作や参照が可能になります。 最後に k8s とその周辺エコシステムは進化が激しく、日々新しい機能やツールが出てきている印象です。 キャッチアップが大変ですが、今後も日々の運用やシステムの安定性に繋がるようなものは時間を見つけて試していきたいと思います。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、サービスエンジニアリング本部の穴澤です。 今年の1月に入社して丁度1年になります。良いタイミングなので1年を振り返ってみたいと思います。 これから転職をしてみようかなと考えている人、新しい事に挑戦したいと思っている人に読んでもらえると幸いです。 1-3月 年始に入社。前日は眠れませんでした。6年働いた前職から転職し、人間関係はゼロから。使っている技術、サービスも違う。大丈夫かなー3ヶ月後生きてるかな?と思っていました。入社後は環境構築しつつ、その後運用業務を上司から少しずつ分けてもらい日々の業務全体をみるようになりました。エンジニアとしての経験を買ってもらって入社したものの、 プログラミング言語 での経験は主に PHP だったので エニグモ での Rails を使った開発に戸惑うところも未だにあります。このタイミングでチームに新しく入ってきた方も居たので受け入れ時に必要な情報やドキュメントを徐々にまとめて自分がサービスの説明や開発フローを説明できるものを準備しました。 この頃はまだ前職の人たちとの飲み会が多く、ホームシック的?な気持ちになったのを覚えています。ああ、転職したんだなあ、という気持ちでした。 EM見習いとして入社したので、入社したその月からメンバーの方と一人ひとり話をする時間を持ち始めました。ただし全然みんながなにやってるのかわからん。という感じでした。 4-6月 「社内での情報共有サービスを移行しよう」という動きがありチームを横断した形でエンジニア数名、関係者含めて小さいチームが発足しました。 私自身が刷新に前向きだったのでやってみようと思い手をあげることになりました。実際には部署をまたいで新ツールの使い方のレクチャや 既存のデータの移行スケジュールの共有、他の部署の人たちへの ヒアリ ングや、もっている課題などを集約しデータ移設などは 山本さん におんぶにだっこでした。 このチームのおかげで普段接点のない部署の方々とお話する機会があったかなと思います。名前もだいぶ覚えてきました。 夏場に大きな施策が控えており、メンテナンスが必要になったため自分が担当になりました。過去メンテのドキュメントを探しマニュアルや手順書の整備をはじめました。 メンテにはインフラエンジニアや他のサーバサイドエンジニアの方もいましたが「入社して半年でメンテ要員わたしで大丈夫かな?」と思いながらテスト環境で手順書の確認やわからないことを少しずつ潰していきました。 7-9月 サーバーメンテナンスは深夜に実行する事となり、夜に関係者が出社しメンテナンスを終えたのは朝でした。そのまま朦朧としたまま会社を出、 ファストフード店 でおいしい朝ビールをいただきました!色々と学びが多い月でした。メンテをしたことでちょっとシステムの全体構成や アーキテクチャ 全般が「わかった気」になったのを覚えています。 開発業務で機能をゴリゴリ書いている身ではないので案件の調整や小さい運用、リリースの調整やドキュメントの整備、問い合わせや MTG 、ちょっとした相談・・・。こういう事に忙殺されているとたまに「わたしエンジニアって言っていいんだっけ?」と悲観的になることもあるのです。なんだか何もしてないな、EMで入ってきてるけどわたし大丈夫かな。と不安になったこともありました。チームの成果を伸ばしたい、組織に貢献したい。なので技術力そのものだけじゃないところで活躍したい。自分の達成チケット数=成果ではないという事を上司に確認したこともありました。その際、「そういう前提であなたを迎えているのですから(大丈夫ですよ)」と言ってくれたのはいまでも覚えています。メンテも経験したことで、この辺りからはっきりと自分の役割を理解してきました。技術的な学習や実際の開発から完全に手をひいたわけではないけれどそこに自分の価値があるわけではない。というのが1年たった気持ちです。 10-12月 リリースに関わる改善をしたいと日頃思っていたので、夏過ぎからまた有志のチームで細々と情報の整理と改善の策を練るところからはじめています。ちょっと不安ですが自分にできることを探していこうと思います。 また春先から頑張ってきた採用活動が実を結び、 新しい人が入社してくれました! 人材育成も過去の経験をそこそこに活かしながら、今わたしの目の前にいる人の顔をみて、自分ができる「お手伝いをしていこう」と思った今日このごろです。 最後に 異なる環境に飛び込むことは色々考える事も多いし悩みも多いです。それでも今年楽しかったな、ここはやりきったな。という達成感はあります。 それと、自分がどんどん自社のサービス・支える人を好きになってると感じます。携われる領域やできることが些細なことでも一つふえると嬉しいものです。 環境や周りの人を強制的に変えることで視野が広まったり自分が少しずつ変わる変化を、来年も楽しんでいこうと思います。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター