TECH PLAY

株式会社エニグモ

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

240

こんにちは。デザイナーの別所です。 本日はenigmoデザイナーの、最近気になっているデザインに関するトピックをご紹介したいと思います。 今回のテーマは「アプリUI編」です! 心地よい使い勝手!映画レビューアプリ「Must」 「Must」は映画/TVファンのための SNS アプリです。 作品のレビューを投稿したり、見たい映画や番組をコレクションとして保存できたり、友達と繋がって他の人がコレクションしている作品を見たりすることができます。 おすすめ作品をデイリーでレコメンドしてもらえたりもします。 2016年にリリースされた アメリ カ生まれのアプリです。 引用: https://mustapp.com/ デザイナーのこだわりを感じるUI/UX Mustの一番の特徴は、画面の下半分の操作で完結するように設計されているUI/UXではないでしょうか。 レビューの入力や「観たい」「鑑賞済み」を選択するボタンなど、このアプリのメインアクションは全て画面の下部で操作できるため、「指が届かない」「両手に持ち替える」などのストレスがなくスムーズに操作できます。 昨今の スマホ の様々な端末サイズに左右されないような設計がされているんですね! ストアのレビューでも優れたUI/UXに対してのコメントが多く見られます。 (レビューでUIについて言及されるアプリって珍しい気がします!) 心地良いUIモーション Mustは見た目の美しさだけでなく、モーションにもこだわりを感じます。 作品のサムネイルをタップすると、本棚から選んで取り出すような雰囲気のモーションがついてます。タップした時にサムネが少し小さくなるところも、ユーザーが押したことを認知させる表現で、細かい技が効いてます。 作品情報のモーダルは作品サムネイルにかぶさる様に画面下から表示されレイヤー感があるので、作品サムネイルと作品情報との優先順位を感じさせる見た目になっています。 テキスト要素をギリギリまで排除したシンプルさ 引用: https://mustapp.com/ 海外アプリっぽいな〜と感じるのがテキスト要素の圧倒的少なさ。 基本的には作品名はテキストで表示させず作品画像のみとなっています。 このシンプルさが美しい作品画像をより際立たせてる気がします。 視認性担保のため小さい画像も無いところが、作品情報を認識しやすく操作しやすいです。 楽しい絵文字遣い 絵文字の使い方にシャレが効いていてオシャレです。 レビューを10段階で入力するときは、数字によって表情が変わります。 こういう小技で入力を楽しめる体験って良いですね。 海外のアプリなので日本語のレビューや、邦題作品はまだ少ないようです。 日本でももっと広まって欲しいです! 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、Corporate IT/Business ITを担当している足立 です。 この記事は Enigmo Advent Calendar 2020 の 24日目の記事です。 エニグモ では1人目のコーポレートIT担当として未着手な社内IT環境をコツコツ整備してます。 世間的には1人情シスと呼ばれるポジションです。 今回は2020年に取り組んだ事を書きたいと思います。 2020年は何と言ってもコロナウィルス 1月後半から徐々に猛威が迫ってきて弊社も2月17日に対応・方針についての MTG が予定されてました。 また、偶然にも同タイミングでSlackの全社チャンネルに 会社に対し何らかの方針、対応があるのかと言う内容の投稿がありました。 今思うと見えない恐怖・不安が爆発した日だと思います。 コロナウィルス 2月17日 緊急招集 MTG 2月17日 夕方 経営陣、 監査役 、部署長、情シスが招集されました。 そこで決まった方針は下記内容になります。 方針の大概要 ①「在宅勤務」の推奨 ②出社勤務の場合は「時差通勤」推奨と「社内感染防止ルール」の厳守 ③本方針は翌営業日からのスタート コロナウィルスの影響により、急遽全社的にリモートワークを一斉にスタートする事になりました。 情シスとしてのMISSIONは リモートワーク環境の構築 リモートワーク環境・・・ 実はリモートワークを本導入してませんでした。 当初の予定だとオフィスが元々国立競技場が近いと言うこともありオリンピックに向けて リモートワークを段階的に試験導入する予定でした。 また、コロナ禍前は情シスとしてリモートワーク開始について反対の立場をとっていました。 反対した理由 当時でもリモートワークで業務出来る環境でありましたが いくつか課題があり中々、賛成する事は出来ませんでした。 代表的な理由としては・・・ 運用、緊急時のフローが無い リモートワークの運用フロー・ルールが構築されていない 重大インシデント 発生した際の緊急連絡フローが確立出来てない ウィルス対策が脆弱 コンシュマー版のウィルス対策ソフトを利用してましたが、リモートワークを実施するには強化すべき部分でした。 やはり管理コンソールが無い為、管理者として各端末の状況が把握しにくい部分が課題でした。 MDM (Mobile Device Management)導入して無い ウィルス対策の話と同様に端末の状況を一括で集中管理出来るソリューションが未導入 運用・緊急時のフロー等は社内で方針を決めるだけなので、直ぐに対応が出来る内容ですが 正直、セキュリティ周りをもう少し強化な環境に整えてから実施したいと言う思いでした。 真っ先にやった事 デスクトップ パソコンのリプレイス 緊急事態に付き兎に角、業務が出来る環境にすべくデスクトップPCのリプレイスに着手しました。 当然、パソコン無ければ仕事が出来ません。 当時、アルバイト・ 派遣社員 が使用していた約20台程の Windows デスクトップPCが 稼働中な為、直ぐにメーカーに連絡し、12~13台ずつ購入し3~4週間程で入れ替え完了させました。 キッティングに関し初めはイメージを作成しクローニングで対応も考えたのですが 作成・検証する時間も無かったのと、 エニグモ では予めインストールするソフトも少ないので 人力による力技で対応しました。 2019年より少しずつノートPC化をしていたので、何とかなった感じです。 デスクトップPC リプレイス WEB会議・問い合わせ対応 Zoomを導入しました。 実は運良くコロナ禍前から導入に向けて動いていました。 導入した理由としては Google Workspace(旧称 G Suite)があるので Google Meet利用出来ましたが、やはり大人数でのWEB会議はZoomの方が安定していたからです。 特に エニグモ では4Mと呼ばれる全体集会が月1回あるので、そこは重視しました。 また、仮想背景機能もあるので従業員のプライバシー保護の観点や リモートコン トロール 機能もあるのが決定打でした。 Zoom導入前にトラブルサポート対応で解決までに3時間かかった事例があったので この機能にはだいぶ助けられました。 Zoom対応ハードウェア DTEN導入 元々社 長室・会議室にAppleTVが設置されておりプロジェクターを使用して 会議資料を投影してました。 Mac はAirPlayを使用して安定的に投影出来ますが、 Windows の場合はAirParrot3と言う シェアウェア を利用してましたが非常に動作が不安定と言う課題がありました。 プロジェクターを利用する際に部屋を暗くする不便さ、動作が不安定な課題を解決する為 Zoom対応ハードウェア DTENを1番大きい会議室と社長室に導入しました。 導入後は Windows でも安定した投影が可能になりZoomRoomsアカウントで 無償アカウントユーザーでも時間無制限でWEB会議を利用出来るようになりました。 デメリットな部分としてはお値段が高いところでしょうか 個人的には気に入っているハードです。 当初は全会議室に導入しようとしていたので2台で止めといて良かったです。 社長室と会議室 電話対応 fondesk モバイルチョイス050 導入 コロナ禍と言えども会社へ電話が鳴る日々でした。 その殆どは業務と関係の無い営業電話や人材紹介の電話で月間300件程着信があり 主にバックオフィス部門にて全ての電話を受電し業務負荷が増え 電話対応する度に集中力が途切れて業務がままならない状況でした。 また、お客様の問い合わせについても迅速に対応出来ない事例も発生し 折返し対応するにも会社支給の携帯電話が無いので個人の携帯電話を利用する人も居ました。 そこで電話受付代行サービス fondeskを導入 導入後は会社代表電話番号をfondeskの番号へ転送し 転送先のオペレーターの人が一時対応し全て折返し対応となります。 対応内容はSlackにて投稿されるので、総務が内容毎に関係部署や個人へ共有する運用になりました。 導入後はオフィスが非常に静かになり、こんなにも効果があるものかと感心しました。 また、個人の電話対応として 楽天 コミュニケーションズの モバイルチョイス050 を導入しました。 導入後は個人 スマホ に050番号を付与する事が出来るので、折返し対応時にプライベートの番号が伝わる事が無い様になりました。 メリットとしては簡単にBYODを導入出来る、データ通信ではなく音声回線を利用しているので音質が安定しているのが良い部分だと思います。 FAX対応 メール送信&Slack連携 電話の次に対応したのでFAXです。 レガシーなシステムと言えども銀行やカード会社のやり取りで使用する為、 何かしら対応が必要になりました。 弊社のFAXは 複合機 で送受信を行っているのですが 仕様を確認したところ受信したFAXをメール送信出来る機能があり直ぐに設定 メール送信されたものはzapierを経由し Google ドライブ(共有ドライブ)へ保存され受信した際にSlackへ通知されるようにしました。 ただ、この構成で欠点があります。 複合機 のFAXメール送信機能は用紙切れになるとメールが送信出来ない仕様でした。 ここは盲点でした。 FAXの構成 冒頭でも触れたウィルス対策 リモートワークを始めるにあたり絶対にウィルス対策(エンドポイントセキュリティ)の強化は最低限譲れないと思ってました。 そもそも Windows は Norton 、 Mac はESETとOS毎に違っている。 しかも、コンシュマー版を利用している状況 Excel でシリアル番号とインストール端末を管理すると言う運用 この状況を打破する為、CrowdStrikeを導入 念願の EDR を導入する事が出来ました。 展開が終わった時は達成感が凄かった・・・・ 何と言っても管理コンソールの存在に感動、アラートが飛んでくる頼もしさ、勝手にアンインストールされない安心感はプライスレス。 ちなみに導入直後に管理コンソールにログイン出来ない事象がありました。 原因は セカンダリ 環境にCrowdStrikeが構築されログインURLが通常と違っていたのが原因でした。 (後日談としてESETのアンインストールを社内にアナウンスしたら、みんな光の速さで削除してました・・・) まとめ 2021年へ その他にも実施した事もまだあるのですが、今回書くことが出来ないのでいつかタイミングあれば書いてみたいと思います。 情シス視点での福利厚生制度を作ったり社内イベントをやったりしました。 今後リモートワークを核としたワークスタイルへの移行にむけてオフィスをリニューアルする予定で、 そこに向けて色々と動いています サーバールーム移設対応 社内ネットワーク対応(有線・無線) 電話回線周り( 光収容 化・Dialpad導入) インターネットFAX導入 受付システム導入 入退出システム導入 座席予約システム導入 AV機器周り対応 また、4月にokta導入が決定しました! 出来たら来年こそは MDM を構築したいと思ってます。 では、以上になります。 ありがとうございました。 明日の記事の担当は 出品審査担当 の 杉山 さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、インフラチームの 加藤 です。 この記事は Enigmo Advent Calendar 2020 の23日目の記事となります。 本記事では、リモートワーク環境のため、擬似 DNS を社内提供したお話をします。 エニグモ では、今年の2月頃から全社的にリモートワークを開始しました。 それに伴いインフラチームでは、リモートワークのネットワーク周りの対応を行いました。 エニグモ が運用しているサーバ群 エニグモ の運用するサーバは、データセンター内に構築したものと AWS のものがあります。 情シスの足立さん が、 SaaS 導入を進めて下さったためオフィス内にサーバはほぼありません。 サーバへの疎通経路 オフィス・リモート環境共に VPN 経由(+ ファイアウォール )で、サーバ群へアクセス可能です。 リモートワーク開始後のサーバアクセスの問題 リモートワーク開始直後から、ネットワーク設定に関するお問い合わせと、インフラチームの対応が発生しました。 VPN や Wifi の設定など、個々人のネットワーク設定の問題は、一度解決すれば再発することは滅多にありませんでしたが、 名前解決 は、何度もお問い合わせが頻発する厄介な課題でした。 なぜ、名前解決がリモートワークの課題だったのか コロナ禍以前 エニグモ では、オフィス環境からサーバへのアクセスを楽にするため、 ファイアウォール でオフィスの グローバルIP を許可し、非エンジニアスタッフの使用するサーバへはサーバの グローバルIP を指定してアクセス可能にしていました。 名前解決がリモートワークの課題となった原因 リモート環境からサーバへは、 VPN 経由でサーバのローカルIPを指定してアクセスする形でした。そのためオフィスとリモート環境では、サーバのアクセス情報が異なりました。 整理すると、以下の形になります。 職場 アクセス元のIP アクセス先のIP リモート環境 スタッフのお家 サーバのローカルIPを指定 オフィス オフィスのGIP サーバの グローバルIP を指定 リモートワーク開始直後は、採用や営業活動、検品作業のため出社が必要なスタッフも居て、シフトで出社日を回していました。出社したスタッフは、Hostsを社内用に修正せねばならず、名前解決関係のお問い合わせが継続していました。 DNS を導入すべきでしたが、 工数 がかかり難しいところでした。 名前解決の課題を解決 Hostsを一元管理できないかと思案していたところ、 SwitchHosts! を発見し利用することにしました。 SwitchHosts!とは Hostsファイルのサーバ管理が可能になる、端末のアプリです。 開発者は、サーバからHostsファイルをダウンロードして端末で使用する形となります。 SwitchHosts!のよかったところ 無料公開されているアプリケーションだったこと。 配布元にリモートサーバが使えて、Hostsファイルを集中管理できたこと。 Mac / Windows でも、同じUIで使えてサポートし易かったこと。 HostsのパーツごとのON/OFFができ、localでHostsを修正できて組み合わせられること。 端末のHostsのバックアップが取れること。 SwitchHosts!を使用した、擬似 DNS の運用 ユースケース に合わせたHostsファイルを作成し、データセンターにNginxコンテナを立てて配布できるようにしました。 これにより、オフィスでもリモート環境でも、簡単に DNS の機能を提供できるようになりました。 SwitchHosts!導入後の課題と解決方法 SwitchHosts!の導入後、名前解決が原因となる問題はさっぱりとなくなりました。 しかし「 AWS 環境における開発に伴い、Hostsの修正が都度必要になる」という課題が残りました。 原因は、以下2つでした。 スポット インスタンス のマシンが再作成された際、Hostsの修正が必要だった。 AWS 環境に、日々サーバが増設されるため、Hostsに追記が必要だった。 一時は、ダウンロード元のHostsファイルを手動で修正していましたが、 無駄 な作業でした。 そこで、Hosts情報の更新 スクリプト を作成し、Hostsファイルの自動更新を実現しました。 以下が、 スクリプト です。 #AWSのマシンには、${Prefix}タグと${Name}タグに、 #マシンのAZやサイト環境を命名規則として持たせています。 #!/usr/bin/env ruby require ' aws-sdk-ec2 ' ec2_client = Aws :: EC2 :: Client .new() #ARGVに、${Prefix}タグの値(マシンのAZやサイト環境を命名規則としたもの)を持たせています。 Prefix_list = ARGV f = { filters:[{ name : " tag:Prefix " , values : Prefix_list }]} reservations = ec2_client.describe_instances(f).inject([]) do | s , list | s += list.reservations end instances = reservations.each.inject([]) do | s , list | s += list.instances end instance_list = [] instances.each do | e | instance_list << { :instance_id => e.instance_id, :vpc_id => e.vpc_id, :public_ip => e.public_ip_address, :private_ip => e.private_ip_address, :name_tag => e.tags.find{| t | t.key == " Name " }.value, :prefix_tag => e.tags.find{| t | t.key == " Prefix " }.value.downcase, } end instance_list.each do | e | hostname = e[ :name_tag ].downcase #あとは、環境に合わせてよしなに加工してHostsファイルに出力します。 end まとめ DNS の機能を低コストで提供できてよかったです❗️ VPN への依存度も減らしたいので、 Google IAPを検証中です。 同じインフラチームの先輩社員 山口さんが、記事にされていました。 old schoolerなネットワークエンジニアがIAP Connectorを試してみた - エニグモ開発者ブログ 最後に 明日の記事の担当は、 情シスの足立さん です。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
今年組織作りに貢献するためにやったこと こんにちは。アプリケーション開発グループの穴澤です。 この記事は Enigmo Advent Calendar 2020 の 22 日目の記事です。 今年の振り返りとともに主に自分が去年から今年にかけて中途で入社した方にやってきたことを書いておきます。 これからチームに新しい人を迎える人や、 中途採用 入社する方、チームの組織づくりに興味がある方に読んでいただければとおもいます。 実際に取り組んだこと オンボーディング資料 開発手法、フロー・チェックリストの読み合わせ 1on1 1.オンボーディング資料 エニグモ では部署問わず、今年から中途で入社した社員向けのzoomでのオンボーディングが開始されました。 簡単にいうと、「各事業部が具体的になにをやっているか」「どんな取り組みをしているか」を中途入社した方に説明します。 入社された人は部署ごとに数回に分けて参加しています。 思えば今年の早い段階からリモートワークが推奨された状況で、中途入社問わず、自分の所属する部署以外の方と顔を見合わせて話をする機会というのが極端に減りました。例えば「先月入社した方ですね、XX部署のXXです!」といったようなコミュニケーションも、廊下やエレベータで顔をあわせたり、ミーティングの終わりですれ違う、お手洗いで顔を合わせるなどのタイミングで行われてきました。それぞれを取るととても些細なことですが、これらを積み重ねた何かが会社の社風や文化を 醸造 する一部になっていたんだなと感じます。 社風や文化をオンラインでも実現するために、このオンボーディング時に自分が紹介する担当として実践していることは以下の点です。細かい箇所は割愛しますが、リモートワークならでは、でしょうか。 説明資料に自部署の社員の氏名とSlack名の紐付け(顔とアイコンと名前とSlack名が一致しない問題) アプリ、インフラ、データ基盤、それぞれについての相談相手としてまずは覚えておいてほしい人をエピソードを添えて紹介(そんなに色々紹介されても覚えられない問題) BUYMA の仕組みで聞きたいこと、相談したいことがあったら、ここで相談してね、のチャットの紹介(何かを相談したくなってもどこだかわからない問題) 参加してくれた方とサービスエンジニアリング本部との具体的な関わり(紹介してもらったけど具体的にどういう関わりがあるんだろう問題) 2.開発手法、フロー・チェックリストの読み合わせ BUYMA というサービスが産声を上げて10年以上たちます。尖った技術を使っているところもあれば枯れているところ、 やや温かみのある仕組みのところもあり、中途で入社するエンジニアの方が躓くところもあるため、主に若手の方対象に週1回程度開発やリリースの際に 気をつけてほしい点を esa にまとめたフロー・チェックリストの読み合わせを行いました。 基本的に開発に関わる問題点は、所属しているチームのリーダーやメンターが答えてくれますが、 彼らに一点集中よりも、誰にきいても答えてもらえる、誰にきいても大丈夫、という雰囲気作りに自分も貢献したかったから です。ましてや、自分の所属するチーム以外のエンジニアと、躓きや悩みを共有する場所はいくつあってもいいと考えています。 時には 検索エンジン チームのエンジニアをゲストに迎えて検索周りの説明をしてもらったり、 チェックリストについては実際トラブルに遭遇したエンジニア に「なぜこのチェック項目があるのか」というのを語ってもらいました。 3.1on1 1on1は前職でも取り組んできたことですが、今年特に自分が取り組んできたことをあげます。 アジェンダ を用意する 序盤はなかなか用意できませんでしたが、後半は実施の1,2日前にいくつか アジェンダ を用意するようにしました。 4,5つ用意してその中のトピックで話が広がれば盛り上がり、割愛してできるだけ 「話が盛り上がる」「相手の指向性、興味関心がわかりそう」 な所を意識してすすめていきました。 1on1は「・・・最近どうですか」というやり方もありますが、1on1を実施するメンバーの身になって考えてみると 「なにを話せばいいんだ」「なにを言われるのか」と身構えられてしまうこともあります。明日はこういう事を話そう。を用意する方が 時間も有意義ですし、もしネガティブな話をされるということが事前にわかっていれば、自分が言いたいことも事前に用意するなど、お互いに準備ができ、結果的に少しの準備で得られる物が大きいと感じました。 会社全体の動きがわかるトピックを アジェンダ に混ぜて話す メンバーの仕事や取り組みと直接関係のない、会社の動きや施策。最近の売上。他部署に入社した人のこんなところが凄いなど。 会社の中での私達の部署の動き、施策以外の取り組みや課題など、自分と会社をつないでいる環境にどんなものがあり、どんな動きをしているか。中途入社の序盤だと、見えにくい組織の動きを定期的に アジェンダ に混ぜて話をするようにしています。   まとめ 今年は、いつもやっていること+αの「工夫する」注力をしました。特に、コロナ禍のリモートワークで感じやすい「孤独感」を和らげるための「皆さんは一人ではないですよ」ということを伝えるために、様々なできることを模索した年だった様に思います。取り組みの結果を 定量 的にはかることはできないので難しいですが、フィードバックを受けながら、来年も少しずつ改善していきたいと思います。 よいお年を。 明日の記事の担当は インフラグループ の 加藤 さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、データアナリストの田中里澄です。 エニグモ ではデータ活用推進室という部署に所属しており、主に他部署が行った施策の効果検証を担当しています。 私は2020年11月に エニグモ に入社したので、今回はその転職活動の中でどうして今の職を選んだのか、また入社後どのような仕事をしているのかを紹介できればと思います。 前職は ライブ配信 サービスを運営している会社で、同じくデータアナリストとして働いていました。 転職理由について前職に対してネガティブなことは一切なく(むしろ今も大好きな会社です)、今の自分は別の会社で経験を積んだ方がいいと判断したためです。 なぜこの内容を書くのか? 理由は社外の方向けと社内の方向けでそれぞれあります。 社外の方にはデータアナリストの転職活動の参考にしてもらいたいため 社内の方にはまだ入社して日が浅い私の自己紹介になるためです。転職活動だけでなく、自分がどんな仕事をできるか/したいかについても書いています。 そもそもデータアナリストとは? ここであくまで自分の考えにはなりますが、データアナリストとはどんな仕事をするのか?を説明します。 読んでくださっている方に知っていただきたいですし、私が転職活動をしたときにも実際に自分の中で整理しました。 その整理のおかげで、無数にある求人(特にデータ〇〇という職種は最近多い)の中でピックアップする際や、面接時に自分がしたいことを説明する際に役立ちました。 データアナリストがどんな仕事をするのかを一言で表すと、「意思決定に必要な情報/ インサイト を提供する仕事」だと思っています。 データを分析する仕事じゃないの?という意見もあるかもしれませんが、分析はあくまで手段であり意思決定をすることが目的だと考えているのでこのような書き方をしました。 もしかするとコンサルという職種でイメージされる仕事に近いかもしれません。(余談ですが、思考法を勉強するためにコンサル向けの本を読むこともあります) 逆にいうと、こういった仕事をする人のことを「データアナリスト」と定義している会社が多いとも言えます。(本質的にはこちらが正しいかもしれません) どんな会社を求めていたのか? 上記で書いたデータアナリストとしての仕事ができることはもちろんですが、さらにそれを具体化した自分のしたい仕事について2点、またそれに関連した仕事の環境について2点合わせて4点求めていることがありました。 どんな意思決定に携わりたいのか 意思決定にも長期的な経営に関するものや日々のオペレーションに関するものなど色々な種類があります。 その中でも私はプロダクトの改善に関する意思決定に携わりたいと考えていました。 なぜならユーザーに喜んでもらえることに直結して実感しやすく、また分析するなかで人がどんな考えで行動するのかを垣間見ることができるためです。 どんなデータを分析するのか データは大きく分けると 定量 と定性の2種類に分けることができますが私はその両方を用いて分析したいと考えていました。 特に 定量 的なデータだけを扱うことが求められる場合もありますが、ユーザーの行動理由を知るためにはそれだけでは分からないことがたくさんあるためです。 他の職種(特に意思決定者)との連携が取りやすい 上の方で書いていたデータアナリストの仕事内容を遂行するためには他の職種の方との連携が大切です。 その中でも特に最終的に意思決定をする方との連携がとても大事だと考えていました。 分析をする前にはどんなことが分かれば意思決定ができるかを摺り合わせ、分析後にはそこで分かったことを共有し議論することで意思決定に付き添うためです。 分析をするためのデータ基盤が整っているか 整っているという言葉が曖昧ですが、例外はあれど日々の分析をする際の前処理やデータの準備に自分の持っているスキル以上のことを求められないことが基準でした。 もちろん後々はデータ基盤の構築などのスキルも伸ばしていきたいのですが、それをすぐに求められて本当にやりたいことに時間が使えないのはしんどいので求める要件に入っていました。 実際に入社してみて感じたこと 最終的に エニグモ 社に決めた理由としては、上記の求めていることを全て満たせていると選考中や内定をいただいた後の面談で感じたからです。 ここでは入ってみて実際に感じたことを紹介します。 分析結果を共有する際に、集計結果だけでなく考察や ネクス トアクションも求められまた歓迎されていい。 求めていた意思決定者との連携について、非常に取りやすいので分析の方針を立てやすい。 データ分析基盤について、基本的に必要なデータがBQにあって扱いやすい。ただし、それぞれのテーブルの定義などがわからないところやGAのデータについての扱い方を調べる必要があったりと、データはあるが扱い方は聞いたり調べたりが必要なので、今後入ってくる方のためにまだまだ整える余地がある。 入ってからどんな仕事をしているか 主に2種類の施策の振り返りとアプリの分析方針を立てる仕事をしています。 施策についてはどちらも私が入社する少し前から始めていたもので、毎月改善を加えてやっていこうという前提のものでした。 なので振り返りでどんな数字を見るのか?というところから任せてもらい、翌月の改善点の提案を出すところまで行っています。 アプリの分析方針については、私が入社するまでは売上というKGIを追いつつ、KPIとして何を追うべきかについて試行錯誤をしていたという状況です。 そこで今後追うべきKPIについて決める仕事を担っており、今は私の提案を元に議論しています。 総じていうと、現在自分のしたいと思っていた仕事ができており今後もまだまだやりたいことが溢れています。 コロナ禍で入社後のコミュニケーションは以前と比較すると難しいなとは感じておりますが、より一層迅速で納得感のある意思決定に貢献できるように日々の業務に努めていければと思っています。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。 今年4月に エニグモ に入社したデータエンジニアの谷元です。 この記事は Enigmo Advent Calendar 2020 の 20日 目の記事です。 目次 はじめに そもそも同期処理とは? Airflowによる同期処理 検証時のコード サンプルをAirflow画面で見ると? 同期遅延なし時のAirflowログ 同期遅延あり時のAirflowログ 同期タイムアウト時のAirflowログ 所感 最後に はじめに コロナ禍の中、皆さんどのようにお過ごしでしょうか。 私はリモートワークを続けてますが、自宅のリモートデスクワーク環境をすぐに整えなかったため、薄いクッションで座りながらローテーブルで3ヶ月経過した頃に身体の節々で悲鳴をあげました。猫背も加速... さて、 エニグモ での仕事も半年以上経過し、データ分析基盤の開発運用保守やBI上でのデータ整備などを対応をさせていただいてますが、今回は社内で利用されているAirflowの 同期処理 の話をしたいと思います。 尚、こちらの記事を書くにあたり、同環境の過去記事 Enigmo Advent Calendar 2018 がありますので、良ければそちらもご覧ください。 そもそも同期処理とは? 例えば、以下のようなデータ分析基盤上でのETL処理があったとします。 処理1. 連携元DBのあるテーブルをDataLake(GCS)へファイルとして格納後、DWH(BigQuery)へロードする 処理2. DWH上のデータを加工後、外部へCSV連携する 上記の処理は順番に実行されないと必要なデータを含めた抽出ができなくなってしまい、外部へデータ連携できなくなります。そのためには処理1の正常終了確認後に処理2を実施するという、同期処理を意識した実装をする必要があります。 例だとシンプルですが、業務上では徐々にETL処理も増えていき、複雑化していきます。 こうした同期処理は、初めはなくても問題にならないことが多いのですが、徐々に処理遅延が発生してタイミングが合わなくなったり、予期せぬ一部の処理エラーが原因で関連する後続処理が全て意図せぬ状態で動き出してしまったりします。そうならないためにも、設計時点で同期について意識することが大事だと思います。 Airflowによる同期処理 Airflowでは ExternalTaskSensor を使用することで実装可能となります。 Airflowのソース上ではdependという用語が使われているので、Airflowの世界では「同期」ではなく、「依存」と呼んだ方が良いのかもしれません。適弁読み替えていただければ... では、Airflowでの同期検証用サンプルコードを作成してみましたので、実際に動かしながら検証したいと思います。尚、Airflowバージョンは1.10.10となります。 先ほどのETL処理を例にして、下記の内容で検証してみます。 実際のファイル出力やDBへのロード処理などはここでは割愛して、 DummyOperator に置き換えてます。 処理1. 連携元DBのあるテーブルをDataLake(GCS)へファイルとして格納後、DWH(BigQuery)へロードする dag_id: sample_db_to_dwh_daily schedule: 日次16:00 tables: TABLE_DAILY_1, TABLE_DAILY_2, TABLE_DAILY_3 処理2. DWH上のテーブルを用いてデータ加工後、外部ツールへCSV連携する dag_id: sample_dwh_to_file_daily_for_sync_daily_16 schedule: 日次17:00 tables: TABLE_DAILY_1, TABLE_DAILY_2, TABLE_DAILY_3 検証時のコード まずは処理1のサンプルコードになります。 from airflow.models import DAG from datetime import datetime from airflow.operators.dummy_operator import DummyOperator dag = DAG( dag_id='sample_db_to_dwh_daily', start_date=datetime(2020, 12, 1), schedule_interval='0 16 * * *' ) tables = ['TABLE_DAILY_1', 'TABLE_DAILY_2', 'TABLE_DAILY_3' ] for table_name in tables: db_to_dwh_operator = DummyOperator( task_id='db_to_dwh_operator_%s' % table_name, dag=dag ) terminal_operator = DummyOperator( task_id='terminal_operator_%s' % table_name, dag=dag, trigger_rule='none_failed' ) db_to_dwh_operator >> terminal_operator 次に処理2のサンプルコードです。 from airflow.models import DAG from airflow.operators.dummy_operator import DummyOperator from airflow.sensors.external_task_sensor import ExternalTaskSensor from airflow.utils.state import State from datetime import datetime, timedelta tables = ['TABLE_DAILY_1', 'TABLE_DAILY_2', 'TABLE_DAILY_3' ] def _dwh_to_file_operator(dag, table_name, depend_external_dag_id, depend_external_dag_execution_delta): dwh_to_file_operator = DummyOperator( task_id='dwh_to_file_operator_%s' % table_name, dag=dag ) sensors = [] sensors.append(ExternalTaskSensor( task_id='wait_for_sync_db_to_dwh_%s' % table_name, external_dag_id=depend_external_dag_id, external_task_id='terminal_operator_%s' % table_name, execution_delta=depend_external_dag_execution_delta, allowed_states=[State.SUCCESS, State.SKIPPED], dag=dag)) for sensor in sensors: sensor >> dwh_to_file_operator terminal_operator = DummyOperator( task_id='terminal_operator_%s' % table_name, dag=dag, trigger_rule='none_failed') dwh_to_file_operator >> terminal_operator args = { 'start_date': datetime(2020, 12, 3) } dag = DAG( dag_id='sample_dwh_to_file_daily_for_sync_daily', default_args=args, schedule_interval='0 17 * * *' ) for table_name in tables: _dwh_to_file_operator( dag=dag, table_name=table_name, depend_external_dag_id='sample_db_to_dwh_daily', depend_external_dag_execution_delta=timedelta(hours=1) ) サンプルをAirflow画面で見ると? 上記2つのDAGをAirflowのWebUIで見ると以下のようになります。 同期処理をするoperatorは「wait_for_sync_db_to_dwh 」です。図でも ExternalTaskSensor のアイコンになってますね。 今回は3テーブルあり、それぞれ並列処理を想定しているため、3つあります。 そして、それぞれ、該当するテーブルのterminal_operator処理完了を確認後、後続処理が実行されます。 では上記の同期処理がAirflowのログでどのように表示されるか見ていきたいと思います。 同期遅延なし時のAirflowログ まずは、既に sample_db_to_dwh_daily が完了していた場合です。 3テーブル毎のログを載せておきます。 TABLE_DAILY_1 ~~ DAG: sample_dwh_to_file_daily_for_sync_daily Task Instance: wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-07 17:00:00 ~~ [2020-12-09 02:00:09,833] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-07T17:00:00+00:00 [queued]> [2020-12-09 02:00:09,840] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-07T17:00:00+00:00 [queued]> [2020-12-09 02:00:09,841] {taskinstance.py:879} INFO - -------------------------------------------------------------------------------- [2020-12-09 02:00:09,841] {taskinstance.py:880} INFO - Starting attempt 1 of 1 [2020-12-09 02:00:09,841] {taskinstance.py:881} INFO - -------------------------------------------------------------------------------- [2020-12-09 02:00:09,848] {taskinstance.py:900} INFO - Executing <Task(ExternalTaskSensor): wait_for_sync_db_to_dwh_TABLE_DAILY_1> on 2020-12-07T17:00:00+00:00 [2020-12-09 02:00:09,851] {standard_task_runner.py:53} INFO - Started process 48063 to run task [2020-12-09 02:00:09,905] {logging_mixin.py:112} INFO - Running %s on host %s <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-07T17:00:00+00:00 [running]> ip-123-456-78-910.dokoka_toku.compute.internal [2020-12-09 02:00:09,919] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-07T16:00:00+00:00 ... [2020-12-09 02:00:09,921] {base_sensor_operator.py:123} INFO - Success criteria met. Exiting. [2020-12-09 02:00:09,924] {taskinstance.py:1065} INFO - Marking task as SUCCESS.dag_id=sample_dwh_to_file_daily_for_sync_daily, task_id=wait_for_sync_db_to_dwh_TABLE_DAILY_1, execution_date=20201207T170000, start_date=20201208T170009, end_date=20201208T170009 [2020-12-09 02:00:19,834] {logging_mixin.py:112} INFO - [2020-12-09 02:00:19,834] {local_task_job.py:103} INFO - Task exited with return code 0 TABLE_DAILY_2 ~~ DAG: sample_dwh_to_file_daily_for_sync_daily Task Instance: wait_for_sync_db_to_dwh_TABLE_DAILY_2 2020-12-07 17:00:00 ~~ [2020-12-09 02:00:09,831] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_2 2020-12-07T17:00:00+00:00 [queued]> [2020-12-09 02:00:09,839] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_2 2020-12-07T17:00:00+00:00 [queued]> [2020-12-09 02:00:09,839] {taskinstance.py:879} INFO - -------------------------------------------------------------------------------- [2020-12-09 02:00:09,840] {taskinstance.py:880} INFO - Starting attempt 1 of 1 [2020-12-09 02:00:09,840] {taskinstance.py:881} INFO - -------------------------------------------------------------------------------- [2020-12-09 02:00:09,846] {taskinstance.py:900} INFO - Executing <Task(ExternalTaskSensor): wait_for_sync_db_to_dwh_TABLE_DAILY_2> on 2020-12-07T17:00:00+00:00 [2020-12-09 02:00:09,848] {standard_task_runner.py:53} INFO - Started process 48062 to run task [2020-12-09 02:00:09,902] {logging_mixin.py:112} INFO - Running %s on host %s <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_2 2020-12-07T17:00:00+00:00 [running]> ip-123-456-78-910.dokoka_toku.compute.internal [2020-12-09 02:00:09,916] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_2 on 2020-12-07T16:00:00+00:00 ... [2020-12-09 02:00:09,918] {base_sensor_operator.py:123} INFO - Success criteria met. Exiting. [2020-12-09 02:00:09,921] {taskinstance.py:1065} INFO - Marking task as SUCCESS.dag_id=sample_dwh_to_file_daily_for_sync_daily, task_id=wait_for_sync_db_to_dwh_TABLE_DAILY_2, execution_date=20201207T170000, start_date=20201208T170009, end_date=20201208T170009 [2020-12-09 02:00:19,832] {logging_mixin.py:112} INFO - [2020-12-09 02:00:19,832] {local_task_job.py:103} INFO - Task exited with return code 0 TABLE_DAILY_3 ~~ DAG: sample_dwh_to_file_daily_for_sync_daily Task Instance: wait_for_sync_db_to_dwh_TABLE_DAILY_3 2020-12-07 17:00:00 ~~ [2020-12-09 02:00:09,829] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_3 2020-12-07T17:00:00+00:00 [queued]> [2020-12-09 02:00:09,837] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_3 2020-12-07T17:00:00+00:00 [queued]> [2020-12-09 02:00:09,837] {taskinstance.py:879} INFO - -------------------------------------------------------------------------------- [2020-12-09 02:00:09,837] {taskinstance.py:880} INFO - Starting attempt 1 of 1 [2020-12-09 02:00:09,837] {taskinstance.py:881} INFO - -------------------------------------------------------------------------------- [2020-12-09 02:00:09,844] {taskinstance.py:900} INFO - Executing <Task(ExternalTaskSensor): wait_for_sync_db_to_dwh_TABLE_DAILY_3> on 2020-12-07T17:00:00+00:00 [2020-12-09 02:00:09,847] {standard_task_runner.py:53} INFO - Started process 48061 to run task [2020-12-09 02:00:09,901] {logging_mixin.py:112} INFO - Running %s on host %s <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_3 2020-12-07T17:00:00+00:00 [running]> ip-123-456-78-910.dokoka_toku.compute.internal [2020-12-09 02:00:09,915] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_3 on 2020-12-07T16:00:00+00:00 ... [2020-12-09 02:00:09,917] {base_sensor_operator.py:123} INFO - Success criteria met. Exiting. [2020-12-09 02:00:09,920] {taskinstance.py:1065} INFO - Marking task as SUCCESS.dag_id=sample_dwh_to_file_daily_for_sync_daily, task_id=wait_for_sync_db_to_dwh_TABLE_DAILY_3, execution_date=20201207T170000, start_date=20201208T170009, end_date=20201208T170009 [2020-12-09 02:00:19,831] {logging_mixin.py:112} INFO - [2020-12-09 02:00:19,831] {local_task_job.py:103} INFO - Task exited with return code 0 注目して欲しい行は下記となります。 ※ 他テーブルのログも同じですので、ここから先は記載を割愛します [2020-12-09 02:00:09,919] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-07T16:00:00+00:00 ... これが同期処理のログになります。 今回は既に正常終了していたため、1回の確認しか行われておらず、その後、後続処理も含めて正常終了してることが分かります。 同期遅延あり時のAirflowログ 次に前提のタスクが遅延して正常終了した場合のAirflowログもみていきたいと思います。 ~~~ DAG: sample_dwh_to_file_daily_for_sync_daily Task Instance: wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-08 17:00:00 ~~~ [2020-12-10 02:00:04,174] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-08T17:00:00+00:00 [queued]> [2020-12-10 02:00:04,182] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-08T17:00:00+00:00 [queued]> [2020-12-10 02:00:04,182] {taskinstance.py:879} INFO - -------------------------------------------------------------------------------- [2020-12-10 02:00:04,182] {taskinstance.py:880} INFO - Starting attempt 1 of 1 [2020-12-10 02:00:04,182] {taskinstance.py:881} INFO - -------------------------------------------------------------------------------- [2020-12-10 02:00:04,189] {taskinstance.py:900} INFO - Executing <Task(ExternalTaskSensor): wait_for_sync_db_to_dwh_TABLE_DAILY_1> on 2020-12-08T17:00:00+00:00 [2020-12-10 02:00:04,192] {standard_task_runner.py:53} INFO - Started process 21683 to run task [2020-12-10 02:00:04,245] {logging_mixin.py:112} INFO - Running %s on host %s <TaskInstance: sample_dwh_to_file_daily_for_sync_daily.wait_for_sync_db_to_dwh_TABLE_DAILY_1 2020-12-08T17:00:00+00:00 [running]> ip-123-456-78-910.dokoka_toku.compute.internal [2020-12-10 02:00:04,259] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 02:01:04,322] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 02:02:04,385] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 02:03:04,448] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 02:04:04,511] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... (省略) [2020-12-10 10:27:29,163] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 10:28:29,226] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 10:29:29,273] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 10:30:29,298] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 10:31:29,360] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_daily.terminal_operator_TABLE_DAILY_1 on 2020-12-08T16:00:00+00:00 ... [2020-12-10 10:31:29,363] {base_sensor_operator.py:123} INFO - Success criteria met. Exiting. [2020-12-10 10:31:29,366] {taskinstance.py:1065} INFO - Marking task as SUCCESS.dag_id=sample_dwh_to_file_daily_for_sync_daily, task_id=wait_for_sync_db_to_dwh_TABLE_DAILY_1, execution_date=20201208T170000, start_date=20201209T170004, end_date=20201210T013129 [2020-12-10 10:31:33,698] {logging_mixin.py:112} INFO - [2020-12-10 10:31:33,698] {local_task_job.py:103} INFO - Task exited with return code 0 先ほどのPoking行がログに表示され続けていることが分かります。今回は途中でタスクを手動で動かして正常終了させました。 尚、Pokingの間隔はExternalTaskSensorの引数で poke_interval を設定すると変更ができました。ソースを見るとデフォルトは60秒のようです。ログとも一致してますね。 同期 タイムアウト 時のAirflowログ 業務では非機能要件も大事だと思います。 Airlfowでは、ExternalTaskSensorの引数で timeout があるようです。 こちらで タイムアウト (default: 1週間ですかね、長っ)を適切に設定して、エラー検知をしてみたいと思います。 sample_dwh_to_file_daily_for_sync_hourly/wait_for_sync_db_to_dwh_TABLE_HOURLY_1/2020-12-09T17:00:00+00:00/4.log. [2020-12-11 18:21:14,539] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_hourly.wait_for_sync_db_to_dwh_TABLE_HOURLY_1 2020-12-09T17:00:00+00:00 [queued]> [2020-12-11 18:21:14,551] {taskinstance.py:669} INFO - Dependencies all met for <TaskInstance: sample_dwh_to_file_daily_for_sync_hourly.wait_for_sync_db_to_dwh_TABLE_HOURLY_1 2020-12-09T17:00:00+00:00 [queued]> [2020-12-11 18:21:14,551] {taskinstance.py:879} INFO - -------------------------------------------------------------------------------- [2020-12-11 18:21:14,551] {taskinstance.py:880} INFO - Starting attempt 4 of 9 [2020-12-11 18:21:14,552] {taskinstance.py:881} INFO - -------------------------------------------------------------------------------- [2020-12-11 18:21:14,563] {taskinstance.py:900} INFO - Executing <Task(ExternalTaskSensor): wait_for_sync_db_to_dwh_TABLE_HOURLY_1> on 2020-12-09T17:00:00+00:00 [2020-12-11 18:21:14,568] {standard_task_runner.py:53} INFO - Started process 84130 to run task [2020-12-11 18:21:14,625] {logging_mixin.py:112} INFO - Running %s on host %s <TaskInstance: sample_dwh_to_file_daily_for_sync_hourly.wait_for_sync_db_to_dwh_TABLE_HOURLY_1 2020-12-09T17:00:00+00:00 [running]> ip-123-456-78-910.dokoka_toku.compute.internal [2020-12-11 18:21:14,639] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_hourly.terminal_operator_TABLE_HOURLY_1 on 2020-12-10T16:00:00+00:00 ... [2020-12-11 18:22:14,701] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_hourly.terminal_operator_TABLE_HOURLY_1 on 2020-12-10T16:00:00+00:00 ... [2020-12-11 18:23:14,764] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_hourly.terminal_operator_TABLE_HOURLY_1 on 2020-12-10T16:00:00+00:00 ... [2020-12-11 18:24:14,828] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_hourly.terminal_operator_TABLE_HOURLY_1 on 2020-12-10T16:00:00+00:00 ... [2020-12-11 18:25:14,892] {external_task_sensor.py:117} INFO - Poking for sample_db_to_dwh_hourly.terminal_operator_TABLE_HOURLY_1 on 2020-12-10T16:00:00+00:00 ... [2020-12-11 18:25:14,898] {taskinstance.py:1145} ERROR - Snap. Time is OUT. Traceback (most recent call last): File "/opt/airflow/venv/lib/python2.7/site-packages/airflow/models/taskinstance.py", line 983, in _run_raw_task result = task_copy.execute(context=context) File "/opt/airflow/venv/lib/python2.7/site-packages/airflow/sensors/base_sensor_operator.py", line 116, in execute raise AirflowSensorTimeout('Snap. Time is OUT.') AirflowSensorTimeout: Snap. Time is OUT. [2020-12-11 18:25:14,899] {taskinstance.py:1202} INFO - Marking task as FAILED.dag_id=sample_dwh_to_file_daily_for_sync_hourly, task_id=wait_for_sync_db_to_dwh_TABLE_HOURLY_1, execution_date=20201209T170000, start_date=20201211T092114, end_date=20201211T092514 [2020-12-11 18:25:15,070] {logging_mixin.py:112} INFO - [2020-12-11 18:25:15,070] {local_task_job.py:103} INFO - Task exited with return code 1 hourlyに変更して実施したログですが、「Snap. Time is OUT.」と良い感じ(?)にエラーとなってくれました。 業務上では、お目にかかりたくないログ内容ですが... 他にも色々あるようですが、この辺りを抑えておけば基本的な使い方は抑えたことになるかと思います。 所感 実際にAirflow同期処理をやってみて思ったのですが、 同期を取る対象のoperatorをどれにするか 同期を取るoperatorの時刻差はどれだけあるか 非機能要件にあった タイムアウト 設定で適切にエラーで落とそう 複雑になるとDAG間の関係性がわかるグラフをWebUIでサクッとみたい が、気になりました。 「同期を取る対象のoperator」は「terminal_operator_<テーブル名>」としました。 ダミー処理なので冗長にはなってしまいますが、各並列処理毎に加えておいたほうがDAG修正時の同期影響を意識しなくて良くなるのかなと思ったためです。 また、DummyOperator利用時に 「trigger_rule='none_failed'」の引数を付け加えないと、先に実行されたケースもありましたので注意が必要そうです。 「同期を取るoperatorの時刻差」ですが、何度も繰り返してると混乱してしまう可能性もあるので、テストでも十分に気をつけて対応しないといけないですよ(to自分)。実際の業務でもtimedeltaで指定する時刻に誤りをテストで見落としてしまい、その結果、リリース後に同期処理が想定時間通りに終わらず遅延してしまい、外部連携のタイミングに間に合わなくなり問題になってしまいました...。 今回の検証例は、お互いdaily実行だったのですが、頻度が異なると慎重な対応が求められそうです。 ただ、この辺の制御はAirflowのコアな制御なので、今後、利用者が意識しないで済むようにdag_idとオプション引数で渡したら、同期を取ってくれるような機能があると良いなとも思いました。利用頻度が多くなると、そういった実装も検討しないといけないのかもしれません。 あとは、WebUI上にてDAGの関係性がグラフで見れると、意図したoperatorで同期が取れているかの確認がやりやすいと思いました。 見方が分からないだけなのか、未実装なのか把握できてないのですが、WebUIをみる限り今回のAirflowのバージョン1.10.10ではなさそうです。 最後に Airflowでの同期処理について少しでも伝わればと思い、このテーマで記事を書いてみました。 本記事を通して少しでもイメージを掴めて頂けますと幸いです。 他にも面白そうな機能はあると思いますので、また、機会があれば投稿したいと思います。 私からは以上となります。最後までお読み頂きありがとうございました。 明日の記事の担当はエンジニアの高山さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。 BUYMA の検索やデータ基盤周りを担当している竹田です。 この記事は Enigmo Advent Calendar 2020 の19日目の記事です。 エニグモ に入社して GCP や AWS といった クラウド サービスを利用することが多くなり、日々刺激を受けながら業務に従事しております。 その中でも Kubernetes のようにシステムを「宣言的」に定義するモデルに技術進化の恩恵を感じており、自分の体験も踏まえて、 クラウド サービスで Kubernetes を一般利用するに至るまでどういう歴史的経緯があるのかを辿ってみたくなりました。 (実際 Kubernetes で編集するファイルも「 マニフェスト (≒宣言)ファイル」といいますね) なお、記事内容には主観を含む部分や、内容を簡素化するため端折っている部分もありますので、あらかじめご了承ください。 chroot まずは Kubernetes で管理されるコンテナの歴史を探ってみたいと思います。 コンテナ技術の前身は1979年に登場した chroot と言われています。 chroot で特定階層以下をroot ディレクト リとすることで、システムとの分離を実現することができます。 変更した ディレクト リ配下には動作中のOSと同じ ディレクト リ構造を実体として持つ必要があり、利用シーンとしては ssh ログインしたユーザに ディレクト リ移動の制限をかけるような場合でしょうか。個人としてはあまり使った記憶はありません。 少なくともリソースの制限を設けることや ディレクト リ階層をイメージのような形で持つことはできませんでした。 コンテナのベース技術 コンテナのベースとなる技術は2000年に FreeBSD から発表された FreeBSD jail という機構です。 カーネル に手を加えてOSレベルでの仮想化機構を実現しており、 ファイルシステム やネットワークなどの分離ができるようです。 この FreeBSD jailは知らなかったのですが、当時は業務で Solaris を利用しており、すでに2005〜2006年頃には Solaris コンテナという概念が出てきていたことを記憶しています。 また、この頃はまだあまり Linux は表舞台には出てきていませんでした。10数年前当時は商用製品であることが重視されていたと思います。 当時の Linux は Solaris などの 商用UNIX と比較すると OSとしての安定性が高くなかった OOM Killerに悩まされたこともしばしば 比較的 スループット が重視されていた とりあえずタスク間がフェアじゃなかった リソース管理や デバッグ 面が弱かった など こういった背景もあり、ミッションクリティカルなシステムでは 商用UNIX を選択するのが一般的でした。 ただ、2000年台初頭から、 OSS(Open Source Software) が台頭してきました。 開発者のニーズにマッチしていたことや、 Red Hat社 のようなビジネスモデルを確立できたことなどが、 OSS を後押しした背景にあると思います。 ちなみにコンテナが出てくるまでの仮想化技術の筆頭は VM(VirtualMachine) ですが、本記事では割愛します。 cgroups Linux におけるコンテナは cgroups が根幹となっています。 カーネル の機構によりプロセスをグループ単位にして、そのグループ内でリソース配分や制限を行う技術です。 少しだけcgroupsには関わっていたこともあり、 Linux が 商用UNIX に追いつきそうだなと感じていた頃でした(おそらく2008〜2009年頃)。 ただし、設定方法が特殊であり、一般利用するにはかなり難易度が高いものでした。 この後、cgroupsを管理できる LXC というソフトウェアが出ています。 ポータビリティ性が低かったのか、使い勝手が良くなかったのか、、なぜかあまり脚光は浴びていませんね。 Docker 言わずと知れたDocker社が開発したコンテナを管理するソフトウェアです。 自分の印象では利用者がcgroupsを意識することなく利用でき、それをコードベースで管理できる柔軟なラッパー、、という解釈です。 ざっくりと表現すると Linux では以下のイメージです。 近年のCPUパフォーマンスや SSD 等によるI/Oパフォーマンスの向上、かつ VM よりも遥かに軽量でポータビリティ性が高く、簡素に利用できるということもあり、主に開発者の間で一気に広まったように思います。 Kubernetes Google 社が2013年に発表した コンテナオーケストレーションツール です。 この当時すでに大量のシステムをコンテナ化していた事実に衝撃を受けたのを覚えています。 kubernetes は紆余曲折ありましたが、現在の標準になっているものと思います。 宣言的な記載により、ブルーグリーンデプロイメントやローリングアップデートが非常に簡素に実現できるようになりました。 宣言的 システムがどういう状態にあるべきかを記述する 問題があった場合もその状態になるよう再構成する 内部アプリの修正やバージョンアップは、修正適用済みコンテナに置き換える 解釈は難しいです。今こうある状態が大事、というニュアンスでしょうか。 対義語としては命令的、ということなので、こうしてああしたらその状態になる、というニュアンスですかね。 思えば、トラブル発生の際はシステムを修復・復旧させることが一般的でした。 ステートレスシステム(状態維持が不要なシステム)では、もはや宣言的アプローチが一般的になっていると思います。 コンテナ、および Kubernetes のサービス利用 コンテナ技術は開発者にとっては非常に都合の良いものでしたが、実サービスでの利用では、となると敷居が高かったように思います。 自分の当時の立場で記載すると、以下のような理由からでした。 枯れた技術ではない リソース分割は VM で十分満足できている Kubernetes をオンプレ環境で管理するのはハードルが高い 新技術を使いたいという理由では上長(ひいては経営層)を納得させられない クラウド サービスによるサポート 各 クラウド サービスでのサポート開始により、サービス利用の敷居が一気に下がったものと思います。 Google GKE、 Amazon EKS、 Microsoft AKS などが挙げられます。 コンテナ オーケストレーション では大量のシステムを管理・運用することに長けており、 クラウド サービスの柔軟性と非常に相性が良いと思います。 マネージドな Kubernetes により管理を簡素化できる サポートを受けられる コスト面も使った分だけ 。。となると使わない手はないだろうな、という印象です。 終わりに 実際にはもっと複雑な背景があるとは存じますが、概要としてはこのような流れかなと思います。 「宣言的」概念はシステムのあるべき形だと感じつつも、まだステートレスなシステムでのベストプ ラク ティスなのかなという感触です。 ステートフルシステム(状態維持が必要なシステム、例えばDBサーバなど)でも利用はできますが、まだちょっと厳しいので今後より良い概念・機能が出てくるのかな、という期待があります。 おじさんエンジニアとしては過去に思いを馳せつつも、これからも技術の ビッグウェーブ には乗っていきたいので日々勉強・キャッチアップが本当に大事ですね。 最後まで読んでいただきありがとうございました。 明日の記事担当はデータエンジニアの谷元さんです。よろしくお願いします! 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
お疲れ様です。インフラチームの山口です。 この記事は Enigmo Advent Calendar 2020 の18日目の記事となります。 2020年はコロナ禍でほぼ全社的にリモートワークになったこともあり、 前職のネットワークエンジニアだった頃のWANやビデオ会議の思い出を思い返す機会が多い一年でした。 強く思い出に残っているのは大概、障害と機器の不具合などのトラブル系しかなく、それだけでお腹いっぱいになる感じです。 話は変わりますが、 アドベントカレンダー は業務から少しずらした内容で書くポリシーなので、現行の業務とはあまり関係ない、リモートワークに伴ってよく起こりがちなネットワークのごまかしの話をします。 今年、私の業務何やってたかなというとEKSの運用サポートとオンプレの保守対応が多かったので、業務からずれた内容をエイヤで書き下します。 1.はじめに 本記事はオフィスおよびオンプレミスのDCのPublicIPのみを ホワイトリスト に登録したS3 バケット 上の画像を、リモートワーク下の各ご自宅から閲覧する方法を考えた際の検討事項を整理したものです。 IAP Connectorをタイトルに入れていますが、本記事内ではZero Trust的な機能は使用しません。 IAP ConnectorはGKEに簡単にデプロイできるNAT箱として使っています。 そのため、Beyond Corp Remote Access やCloud IAPについても本記事では特に触れません。 2.問題設定と前提条件 問題設定とその問題を解くにあたっての前提条件(おもに弊社の VPN 関連の構成など)を説明します。 まず、何をやりたいかの問題設定を概説し、その後に付随する前提条件を説明します。 2-1.問題設定 解きたい問題を以下に記載します。 オフィスとDCのPublicIPを ホワイトリスト に登録したS3 バケット 上の画像を、リモートワーク下の自宅から閲覧できるようにしたい エンジニア以外のメンバーも閲覧できるようにしたい 2-2. 前提条件 2-1で説明した問題設定に加えて方法検討の際の制約になりうるNW構成などを思いつく限りに記載します。以降の節でなんやかんや本節の理由を参照して、対応案を絞っていきます。 オンプレミスのDCにある VPN サーバで L2TP / IPSec を終端する VPN 接続後のクライアントPCの経路はスプリットトンネリング(トンネルインタフェースにはデフォルトルート向けてない) クライアントPCは、エンジニアは Mac だが非エンジニアは Windows VPN サーバからクライアントに経路をpushすることは可能 オンプレミス環境は パブリッククラウド に移行中のため余計なリソースを増やしたくない また、概要を簡単に記載した図を以下に示します。 図1 3.方法検討 検討した方法を以下表と図に記載します。 方法は3種類に大別されます。タイトルにIAP Connectorを試してみたと記載しているタイトルの通り最終的には、3bを選択することになるのは明白ですが、建前として各案のPros/Consを考えていきます。 案 小項目 方法 案1.愚直案 S3 バケット のIPアクセス制限に各自の自宅のPublicIPを登録する 案2.ルーティングでごまかし案 2a VPN サーバでS3向けの経路をPushしDC経由にする 2b sshuttleなどを使用しクライアント側でルーティングを調整する 案3.Proxy建てる案 3a DCにProxyを建てる 3b IAP ConnectorをProxyとして建てる&S3 バケット 側の ホワイトリスト に追加 図2 案1.愚直案 S3 バケット 側で愚直に各ご自宅のPublicIPを ホワイトリスト 登録すればいいだけというのは、そのとおりでそれで済むならこの記事をグダグダ書いてる意味ないじゃんという形になってしまうので、半ば無理矢理感はありますが一旦は却下します。 これは人によって意見分かれそうですが、プライベートで契約しているPublicIPの情報をCloudFormationのテンプレートなどに書いて、Gitの履歴に残したくなさがあるので個人的には微妙かなという印象が強いです。 pros 一番シンプル cons アクセスする社員の数増えたら運用手間かもしれない。 そもそも、ご自宅が固定IPとは限らないケースもある。 現状はCloudFormationでS3 バケット 管理しているが、個人の自宅のPublicIPをテンプレートに残してしまうのって良いのか? なんか嫌じゃない? 案2. ルーティングでごまかし案 ルーティングでごまかし案は2a、2bに分かれます。 2aと2bの違いはルーティングの調整を VPN サーバ側で行うか、クライアントのPC側で行うかの違いです。 案2での基本方針は以下からS3で使用されるPublicIPのレンジを確認してそのIPレンジ向けの通信をDC経由にします。 AWS IP address ranges 2a、2bのpros/consを記載します。 正直自分しか使わないのなら他の環境に影響少なくて手軽な2bでローカルのPCでルーティングいじってごまかすぞ、となるのですが、 今回の要件的には他メンバーにも展開する可能性があるので却下します。 記事まとめている途中で、わざわざS3のPublicIPのレンジ全部経路切らなくても DNS 引いた結果をhostsに追加&それ向けのホストルートをトンネルインタフェースに切れば良いんじゃないかと思いましたが他の人に展開するという観点ではやっぱり却下です。 2a: VPN サーバでS3向けの経路をPushしDC経由にする pros クライアントPC側の作業は楽 cons この要件のためだけに VPN サーバの経路調整したくない S3のPublicIPのレンジをすべてDC経由にした場合に意図しない影響って本当に出ないんだっけ? 2b: sshuttleなどを使用しクライアント側でルーティングを調整する pros VPN サーバ側の設定を変えなくて済む 当該S3 バケット を参照したい人だけDC経由になるので、2aより意図しない副作用は少なそう cons エンジニアならローカルで経路調整したりできそうだけど、非エンジニアの人には難しいかもしれない 案3. Proxy建てる案 Proxy建てる案はどこにProxyを建てるかどうかで3a、3bに分かれます。 3aはDCにProxyを建てる案です。DCにnginxのProxy建ててアクセスしている実績はあるのですが、オンプレのリソースは極力減らしていっている状況なので却下します。 3a: DCにProxyを建てる pros VPN のルーティングは調整しなくて済む 同様のProxyはDCにも何台かすでにある cons DCに余計なリソースが増える 運用手間 3b: IAP ConnectorをProxyとして建てる&S3 バケット 側の ホワイトリスト に追加 pros VPN のルーティングは調整しなくて済む Deployment Managerでデプロイは楽にできそう cons 動作確認は必要 GKEの クラスタ がデプロイされるのでコスト面は大丈夫? 方法決定 一旦案をまとめます。 5案だしましたが、以下の3案が確度高く現実的には行けそうな感じがします。 案1、案2bはこの検討時点では動作することが分かっていたので、案3bの構成の検討を次節で行います。 案3bの検討事項ではコスト面と動作確認がconsとして上がっていますが、本記事ではとりあえず動作確認だけを行います。 案1.愚直案 とりあえずはこれできるけど面白みはない 案2b: sshuttleなどを使用しクライアント側でルーティングを調整する エンジニアは拾えるかつ楽だけど非エンジニアは拾えない 案3b: IAP ConnectorをProxyとして建てる&S3 バケット 側の ホワイトリスト に追加 検証はした方が良いが意外と楽にできそう 4.リソース作成・動作確認 本節では 案3b: IAP ConnectorをProxyとして建てる&S3バケット側のホワイトリストに追加 の説明を簡潔に行います。 IAP Connectorのデプロイは基本的には以下公式の手順に従います。 オンプレミス アプリでの IAP の有効化 最終的な構成図を以下に示します。 構成上、Deployment Managerのテンプレートでは、IAP ConnectorのNAT後のPublicIPとGCLBに アサイ ンするPublicIPも同じテンプレートでデプロイされてしまうようですが、以下の理由から、PublicIPはコンソールでリソースを作成しそれをテンプレートから参照するように修正しました。また、とりあえずぱっとデプロイできて動きだけ確認できればいいので、Terraformで作成する選択肢は取らず、雑にDeployment Managerのテンプレート一枚で作成しました。 GKE クラスタ はなにか問題が出たら潰して再構築する程度の温度感の運用を想定 ambassadorのPodの問題判別や クラスタ 運用は極力しない手抜き前提 PublicIPはGKE クラスタ と合わせて潰れてしまうとS3 バケット の ホワイトリスト 設定や DNS 設定が手間 所謂ライフサイクル異なるリソースは別テンプレートにというやつ(今回PublicIPは CLI でリソース作ったけれど) 図3 5.まとめ 本記事では、IAP Connectorを容易にデプロイできるNAT箱として利用しました。 Cloud IAP側でZero Trust的な制御を入れたとしても、IAP ConnectorからS3 バケット のアクセス制御は昔ながらのIP制限方式になってしまっているなど、ツッコミどころや深堀りする余地はありますが、 S3 バケット の前段に設置する構成については、一応当初の目的通りの動作を確認できました。 また、この記事とは関係ない半ば余談の愚痴ですが、2020年の夏頃にIAP Connectorを検証したのですが、プロキシ先からのレスポンスの書き換えができなくて困って、IAP Connectorに対する熱が下がったという個人的な経緯もあり、 VPN もしくは既存のProxyの置き換え手段として手放しで愚直に推せる仕組みではないなというのが率直な印象でした。ただ、活用できる局面は多そうなので、引き続き継続的にキャッチアップしていきたいなと思います。 しかしながら、面倒でもGKEにnginxのProxyを建ててそれをCloud IAPで守った方がIAP Connectorより融通聞くのではという気持ちもあり、目的とそれにかける手間のバランスを見極めるのは難しいなと思いました。結論は毒にも薬にもならない感じなのですが終わります。 明日の記事の担当は、データテク ノロ ジー グループの竹田さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、主に検索周りを担当しているエンジニアの伊藤です。 この記事は Enigmo Advent Calendar 2020 の 17 日目の記事です。 みなさんは適切なDockerfileを書けていますか?とりあえずイメージのビルドが出来ればいいやとなっていませんか? 今回は自戒の意味も込めて、改めてDockefileのベストプ ラク ティスについて触れつつ、 そもそもDockerfileを書かずにコンテナイメージをビルドする方法とコンテナセキュリティに関する内容についてまとめてみました。 Dockerfileのベストプラクティス イメージサイズは極力小さくしよう ビルドキャッシュを活用しよう Dockerfileに関する悩みどころ Dockerfileを書かないという選択肢 Buildpack Cloud Native Buildpacks CNBの仕組み デモ CNBのメリット セキュリティについて 概要 コンテナにおけるセキュリティ基準 コンテナの脆弱性スキャン ツールの活用 dockle trivy デモ Dockerfileベース CNBベース CIへの組み込み まとめ Dockerfileのベストプ ラク ティス ご存知の方も多いと思いますが、 こちら がDocker社が推奨するベストプ ラク ティとなっています。 せっかくなので事例を交えていくつかピックアップしてみます。 イメージサイズは極力小さくしよう 軽量なベースイメージを選択する Docker社の推奨は debian 不要なパッケージはインストールしない レイヤはなるべく減らす RUN/COPY/ADDだけがレイヤを増やすのでこれを使用するときに意識しましょう。 RUNで実行するコマンドは極力 && で連結する 可能な場合は マルチステージビルド を利用する ADDを使用した アンチパターン (下記の例ではADDによって圧縮ファイルを含んだレイヤが余計に作成されてしまう) アンチパターン ADD http://example.com/big.tar.xz /usr/src/things/ RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all 推奨例 RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.xz \ | tar -xJC /usr/src/things \ && make -C /usr/src/things all ビルドキャッシュを活用しよう イメージをビルドするとき、DockerはDockerfileに書かれた命令を上から順番に実施します。 その際、各命令毎にキャッシュ内で再利用できる既存のイメージを探しますが、なければ以降のキャッシュは破棄されます。 そのため、更新頻度が高いものをDockerfileの後ろの方に記載することが重要になります。 例えば下記は app というアプリケーションコードを含む ディレクト リをコンテナにコピーし、 pip installによって必要なライブラリをインストールする例です。 アンチパターン COPY app /tmp/ RUN pip install --requirement /tmp/requirements.txt 推奨例 COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY app /tmp/ 一見すると前者の方がレイヤが少ない分、良さそうに見えますが、 app 配下のコードに変更が入るたびにライブラリのインストールも行われ、 その分ビルド時間が伸びてしまいます。 Dockerfileに関する悩みどころ ここまでDockerfileに関するベストプ ラク ティスについて触れてきましたが、Dockerfileを作成、メンテするのって大変ではないですか? どのベースイメージを使用すべきか? イメージサイズが大きくなりすぎる イメージサイズの削減を頑張ってたら時間が溶けた(開発作業に専念したいのに。。。) Dockerfile自体のメンテが辛い イメージサイズを小さくしようと思うとDockerfile自体の可読性が下がるというつらみ ベストプ ラク ティスを意識することが自体が辛い セキュリティ的な懸念 使用するベースイメージに 脆弱性 が含まれていないかなど Dockerfileを書かないという選択肢 そこで続いてのお話がBuildpackについてです。 こちらを利用することでDockerfileを書くことなく、 ソースコード からコンテナイメージを生成することが可能になるというものです。 Buildpack 2011年にHerokuが考案し、 Cloud Foundry 、 Gitlab 、 Knative 等で採用されている仕組み 例えばGitlabでは Auto DevOps(Auto Build) で利用されています 様々な言語のBuildpackを使ってユーザのアプリケーションコードに対して、「判定」、「ビルド」、「イメージ化」といった一連の流れを実施する事によって、基盤上で動作可能な形にアプリケーションコードを組み立てる Cloud Native Buildpacks 上記のHerokuオリジナルと呼ばれるBuildpackが特定の実 行基 盤でしか動作しないというでデメリットがあったのに対し、Dockerの急速な普及を背景に、OCIイメージのようなコンテナ標準を採用したイメージを作成しようと始まったのがCloud Native Buildpacks(以降 CNBと略) Projectです。 HerokuとPivotalが中心となって2018年1月にCNCF傘下でスタートし、現時点で CNCFのSandboxプロジェクト という立ち位置になっています 以降はこちらのCNBについての概要について記載します CNBの仕組み CNBを利用してイメージを生成する際は ビルダー というものを指定します。 ビルダーはアプリのビルド方法に関するすべての部品と情報をバンドルしたイメージとなっており、 複数のbuildpack 、 lifecycle 、 stack で構成されています。 公式サイトから引用 buildpack ソースコード を検査し、アプリケーションをどうビルドし実行するかを決める lifecycle buildpackの実行を調整し、最終的なイメージを組み立てる stack ビルド及び実行環境用のコンテナイメージのペア デモ 基本的にCNBを利用して運用していく際には、自前のビルダーを作成することになると思います。 今回はお試しということで、すでにあるビルダーを使って試してみたいと思います。 前提条件 ローカル環境に Docker 及び Buildpack がインストール済みであること サンプルコード Flaskを利用したWebアプリケーション(単純に Hello World と出力するだけのもの) 構成としては下記の通りで最低限のファイルのみ配置しています。 . ├── requirements.txt └── src ├── __init__.py ├── app.py └── templates └── index.html ビルド それではpackコマンドを使ってビルドしてみましょう。 $ pack build sample-cnb:0.0.1 Please select a default builder with: pack set-default-builder <builder-image> Suggested builders: Google: gcr.io/buildpacks/builder:v1 Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python Heroku: heroku/buildpacks:18 heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP Paketo Buildpacks: paketobuildpacks/builder:base Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, Ruby, NGINX and Procfile Paketo Buildpacks: paketobuildpacks/builder:full Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, PHP, Ruby, Apache HTTPD, NGINX and Procfile Paketo Buildpacks: paketobuildpacks/builder:tiny Tiny base image (bionic build image, distroless-like run image) with buildpacks for Java Native Image and Go Tip: Learn more about a specific builder with: pack inspect-builder <builder-image> packコマンドを実行すると上記のようにビルダーを指定しろと言われます。 今回はここでおすすめされている Google Cloud Buildpacks を利用して実行します。 $ pack build sample-cnb:0.0.1 --builder gcr.io/buildpacks/builder:v1 v1: Pulling from buildpacks/builder Digest: sha256:f0bb866219220921cbc094ca7ac2baf7ee4a7f32ed965ed2d5e2abbf20e2b255 Status: Image is up to date for gcr.io/buildpacks/builder:v1 v1: Pulling from buildpacks/gcp/run Digest: sha256:83eb67ec38bb38c275d732b07775231e7289e0e2b076b12d5567a0c401873eb7 Status: Image is up to date for gcr.io/buildpacks/gcp/run:v1 ===> DETECTING google.python.runtime 0.9.1 google.python.missing-entrypoint 0.9.0 google.utils.label 0.0.1 ===> ANALYZING Previous image with name "sample-cnb:0.0.1" not found ===> RESTORING ===> BUILDING === Python - Runtime (google.python.runtime@0.9.1) === Using runtime version from .python-version: 3.7.8 Installing Python v3.7.8 Upgrading pip to the latest version and installing build tools -------------------------------------------------------------------------------- Running "/layers/google.python.runtime/python/bin/python3 -m pip install --upgrade pip setuptools wheel" Collecting pip Downloading pip-20.3.1-py2.py3-none-any.whl (1.5 MB) Collecting setuptools Downloading setuptools-51.0.0-py3-none-any.whl (785 kB) Collecting wheel Downloading wheel-0.36.2-py2.py3-none-any.whl (35 kB) Installing collected packages: pip, setuptools, wheel Attempting uninstall: pip Found existing installation: pip 20.1.1 Uninstalling pip-20.1.1: Successfully uninstalled pip-20.1.1 Attempting uninstall: setuptools Found existing installation: setuptools 47.1.0 Uninstalling setuptools-47.1.0: Successfully uninstalled setuptools-47.1.0 Successfully installed pip-20.3.1 setuptools-51.0.0 wheel-0.36.2 Done "/layers/google.python.runtime/python/bin/python3 -m pip inst..." (6.427479028s) === Python - pip (google.python.missing-entrypoint@0.9.0) === Failure: (ID: 194879d1) Failed to run /bin/build: for Python, an entrypoint must be manually set, either with "GOOGLE_ENTRYPOINT" env var or by creating a "Procfile" file -------------------------------------------------------------------------------- Sorry your project couldn't be built. Our documentation explains ways to configure Buildpacks to better recognise your project: -> https://github.com/GoogleCloudPlatform/buildpacks/blob/main/README.md If you think you've found an issue, please report it: -> https://github.com/GoogleCloudPlatform/buildpacks/issues/new -------------------------------------------------------------------------------- ERROR: failed to build: exit status 1 ERROR: failed to build: executing lifecycle: failed with status code: 145 今度は上記のようなエラーが出力されます。 どうやらDockerfileのentrypointに相当する GOOGLE_ENTRYPOINT を設定する必要があるようです。 該当のオプションを追加して下記の通り再トライしてみます。 $ pack build sample-cnb:0.0.1 --builder gcr.io/buildpacks/builder:v1 --env GOOGLE_ENTRYPOINT="flask run --host 0.0.0.0 --port 5000" 〜省略〜 Adding cache layer 'google.python.pip:pip' Adding cache layer 'google.python.pip:pipcache' Successfully built image sample-cnb:0.0.1 上記のように Successfully と出力されれば無事にコンテナイメージのビルドは完了しています。 作成されたイメージを確認してみましょう。 REPOSITORY TAG IMAGE ID CREATED SIZE sample-cnb 0.0.1 4c60a192da62 40 years ago 289MB sample-cnb というイメージが作成されていることが確認できました。 ここで気になるのは作成日が 40 years ago となっていることです。 これについては 公式サイト に記載がありましたが、 どうやら再現可能なビルドを目的とした意図的な設計のようです。 コンテナ起動 ビルドしたコンテナを起動して正常に動作することを確認します。 下記コマンドでコンテナを起動して、 $ docker run --rm -p 5000:5000 -e FLASK_ENV=development sample-cnb:0.0.1 こちら にアクセスすると、下記の画面が表示されることが確認できました。 Dockerfileを使ったビルド 最後に比較のためにDockerfileを利用したビルドも行います。 Dockerfileの準備 FROM python:3.7 WORKDIR /app COPY requirements.txt /app RUN pip install -r requirements.txt COPY src /app/ ENV FLASK_APP=/app/app.py ENTRYPOINT ["flask", "run"] CMD ["--host", "0.0.0.0", "--port", "5000"] ビルド $ docker build -t sample-df:0.0.1 . 比較 Dockerfileベースでビルドしたイメージは下記の通りとなります。 CNBで作成したイメージの方が軽量なOSが利用されていることが分かります REPOSITORY TAG IMAGE ID CREATED SIZE sample-df 0.0.1 9a5c14fd1846 14 seconds ago 928MB CNBのメリット CNBのメリットをざっとまとめると下記のような感じになるかと思います。 開発に注力できる 開発者はDockerfileを作成、メンテすることから開放される 持続可能な運用 スケーラブルなセキュリティ対応 散在しがちなDockerfileすべてにおいて 脆弱性 対応などしていくのは現実的ではない セキュリティについて 私のコンテナセキュリティに対する知識としては、下記のようなレベルのものでした。 コンテナにおけるセキュリティって何すればいいの? そもそもコンテナに限らず何をすればセキュリティちゃんとしてますって言えるの? という訳でコンテナにおけるセキュリティ基準やツールとしてはどういったものがあるのかを調査した結果をまとめます。 概要 コンテナにおけるセキュリティ基準 CISベンチマーク 1 セキュリティに関する基準を容易に実行可能な ベンチマーク として提供 ここ の説明が分かりやすかったです NIST コンテナセキュリティに関する基準 コンテナの 脆弱性 スキャン コンテナ環境もオンプレ同様にOSのライブラリやパッケージなどから構成されるため、これまで通り 脆弱性 対策が必須である それに加えてコンテナイメージ、ランタイム環境の 脆弱性 にも配慮する必要がある ツールの活用 とりあえず手軽に上記のセキュリティ基準チェックと 脆弱性 スキャンを行いたいというモチベーションの元、以前から気になるツールをピックアップしました。 dockle https://github.com/goodwithtech/dockle 概要 CIS Benchmark に対応 ベストプラクティス のチェック 使い方 dockle [イメージ名] trivy https://github.com/aquasecurity/trivy 概要 コンテナの 脆弱性 スキャンツール 使い方 trivy [イメージ名] デモ ここで上記で作成したコンテナイメージ(Dockerfileから作成したイメージとCNBで作成したイメージ)をそれぞれのツールにかけた場合にどういった結果になるか確認してみたいと思います。 Dockerfileベース まずはDockerfileからビルドしたイメージの方です。 dockle WARNレベルが1件検知されました。 $ dockle sample-df:0.0.1 WARN - CIS-DI-0001: Create a user for the container * Last user should not be root INFO - CIS-DI-0005: Enable Content trust for Docker * export DOCKER_CONTENT_TRUST=1 before docker pull/build INFO - CIS-DI-0006: Add HEALTHCHECK instruction to the container image * not found HEALTHCHECK statement INFO - CIS-DI-0008: Confirm safety of setuid/setgid files * setuid file: usr/bin/chfn urwxr-xr-x * setgid file: usr/bin/ssh-agent grwxr-xr-x * setuid file: usr/lib/openssh/ssh-keysign urwxr-xr-x * setuid file: bin/umount urwxr-xr-x * setgid file: usr/bin/wall grwxr-xr-x * setuid file: bin/mount urwxr-xr-x * setuid file: usr/bin/gpasswd urwxr-xr-x * setuid file: usr/bin/passwd urwxr-xr-x * setgid file: usr/bin/chage grwxr-xr-x * setuid file: bin/su urwxr-xr-x * setuid file: bin/ping urwxr-xr-x * setgid file: usr/bin/expiry grwxr-xr-x * setuid file: usr/bin/newgrp urwxr-xr-x * setuid file: usr/bin/chsh urwxr-xr-x * setgid file: sbin/unix_chkpwd grwxr-xr-x trivy こちらは大量の出力結果が表示されるためサマリのみ貼っておきます。 CRITICALなものが69件検知されていることが分かります。 $ trivy sample-df:0.0.1 sample-df:0.0.1 (debian 10.2) ============================= Total: 2401 (UNKNOWN: 23, LOW: 1291, MEDIUM: 520, HIGH: 498, CRITICAL: 69) CNBベース 続いてCNBでビルドしたイメージの方を確認してみます。 dockle こちらはWARNレベルのものは1件もなく、INFOレベルのものだけが検知されました。 $ dockle sample-cnb:0.0.1 INFO - CIS-DI-0005: Enable Content trust for Docker * export DOCKER_CONTENT_TRUST=1 before docker pull/build INFO - CIS-DI-0006: Add HEALTHCHECK instruction to the container image * not found HEALTHCHECK statement INFO - CIS-DI-0008: Confirm safety of setuid/setgid files * setgid file: usr/bin/expiry grwxr-xr-x * setuid file: bin/umount urwxr-xr-x * setgid file: usr/bin/chage grwxr-xr-x * setuid file: usr/bin/newgrp urwxr-xr-x * setgid file: usr/bin/wall grwxr-xr-x * setuid file: usr/bin/chsh urwxr-xr-x * setuid file: bin/su urwxr-xr-x * setuid file: usr/bin/passwd urwxr-xr-x * setuid file: usr/bin/gpasswd urwxr-xr-x * setuid file: usr/bin/chfn urwxr-xr-x * setuid file: bin/mount urwxr-xr-x * setgid file: sbin/unix_chkpwd grwxr-xr-x * setgid file: sbin/pam_extrausers_chkpwd grwxr-xr-x trivy こちらもサマリのみ貼りますが、CRITICALに関しては0件となっています $ trivy sample-cnb:0.0.1 2020-12-14T19:22:18.244+0900 INFO Detecting Ubuntu vulnerabilities... sample-cnb:0.0.1 (ubuntu 18.04) =============================== Total: 75 (UNKNOWN: 0, LOW: 53, MEDIUM: 20, HIGH: 2, CRITICAL: 0) この結果からも Google Cloud Buidpackを利用してビルドしたイメージの方が軽量かつセキュアな環境であることが分かると思います。 CIへの組み込み 上で紹介したツールはいずれもCIに組み込んで使用することも想定して作られています。 下記のようにオプションを指定して使うことで、CIのタイミングで実行&確認がしやすくなっています。 dockle dockle --exit-code 1 [イメージ名] trivy trivy --exit-code 1 --severity CRITICAL --no-progress [イメージ名] まとめ 今回はDockerfileのベストプ ラク ティスのおさらいと、CNBを利用したコンテナイメージのビルド方法、セキュリティに関してさらっとまとめてみました。 今後もコンテナベースのアプリケーション開発が進むと、 これまで個人、チームレベルで任せていたDockerfileの作成、管理が破綻するのではと感じました。 CNBには組織として統制のとれたコンテナ作成やセキュリティ基準を継続的に満たすことの手段が提供されているので、 その辺りをうまく活用していく必要性を感じでいます。 セキュリティについても検知の仕組みだけでなく、日々の運用の中でいかに対応していくかということが大事だと思うので、 今後も試行錯誤しながら少しずつ前進していければと思っています。 明日の記事の担当はインフラエンジニアの山口さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co CIS(Center for Internet Security)とは、米国の NSA (National Security Agency/ 国家安全保障局 )、DISA(Difense Informaton Systems Agency/国防情報システム局)、NIST(National Institute of Standards and Technology/米国立標準技術研究所)などの米国政府機関と、企業、学術機関などが協力して、インターネット・セキュリティ標準化に取り組む団体の名称 ↩
アバター
はじめに こんにちは、サーバーサイドエンジニアの @hokita です。 この記事は Enigmo Advent Calendar 2020 の 16 日目の記事です。 弊社が運営する BUYMA は現状モノレポで管理されており、10年以上も運営しているサービスなのでソースも肥大化していて、メンテナンスが難しくなってきました。 そこで現在、本体から少しずつマイクロサービスに切り離していこうとしています。 その取組の中で配送処理の一部をマイクロサービス化する作業に携わることができました。今回は BUYMA 本体と配送サービスとの通信にイベント駆動 アーキテクチャ を導入した話をしていきます。 イベント駆動 アーキテクチャ マイクロサービスでサービスを切り分ける場合、それぞれ責務が分かれるように分割するかと思います。 しかしサービス間の通信手段によっては各サービスが密になる恐れがあります。 そこでイベント駆動 アーキテクチャ を利用します。 PublisherとSubscriber メリットとして PublisherはSubscriberのことを知らない Subscriberはイベントのこと以外知らない Publisher1つに対し、複数のSubscriberを設置できる このようにPublisherとSubscriberはお互い知らないので、 疎の関係を保つ ことができます。 非同期メッセージングサービス イベント駆動 アーキテクチャ を実現するために、非同期メッセージングサービスを利用します。 代表的なものとして Amazon SNS / Amazon SQS Google Cloud Pub/Sub Apache Kafka などがあります。 今回は Amazon SNS / Amazon SQS を使用しました。 Amazon SNS docs.aws.amazon.com 発行者からサブス クライバー (プロデューサーおよびコンシューマーとも呼ばれます) へのメッセージ配信を提供するマネージド型サービスです。 メッセージを発信するものと思って頂ければと思います。メッセージの受け取り先(サブス クライバー )として今回は Amazon SQSですが、他にも AWS Lambdaなどでも取得することが可能です。 なぜ SNS を利用するのか 次の図を見て頂ければ分かる通り、 SNS がないと BUYMA 本体が配送サービスを 知っている ことになります。つまり 密 になってしまいます。 SNS なしの場合 Amazon SQS docs.aws.amazon.com メッセージキューイングサービスです。同じ非同期処理としてsidekiqやResqueを使っているサービスも多いかと思います。 主な特徴としては "at least once"(最低1回)が保証されている 逆に2回以上同じメッセージを取得する可能性がある Redisのようにジョブを失うことがない 順不同(それなりに担保されるのかと思っていましたが全く順不同でした。) FIFO (First-In-First-Out)キュー 通常キューとは別で FIFO キューというものも使用することができます。 受信する順序が保持される 必ず1回処理される 1つずつ処理される 通常キューより処理が遅くなる 処理順序が決まっている処理(例えば製品価格の変更処理など)で便利かと思います。 ロングポーリング ショートポーリングとロングポーリングというものがありますが、基本ロングポーリングが良いです。(ショートポーリングはいつ使われるのだろうという感じです。) ロングポーリングですが最大20秒キューにメッセージがないか待機をして、あれば即座に実行します。(筆者は最初20秒間に溜まったメッセージを処理するのかと思っていましたが、勘違いでした。メッセージを取得したら即座に実行です。) Shoryuken github.com Rails でSQSをジョブキューとして利用するときは現状Shoryuken一択です。Shoryukenを起動することでSQSのキューをポーリングし、キューを取得して処理を実行してくれます。エンキューももちろんできます。 一通り wiki に必要な情報が書かれているので、そちらを読めば問題なく実装できるかと思います。 以下設定ファイルや実装例を記載します。 shoryuken.yml 対象のキューの情報を記載します。 Shoryuken options · phstc/shoryuken Wiki · GitHub # config/shoryuken.yml groups : purchase_completed : concurrency : 1 queues : - [ 'purchase_completed_queue' , 1 ] pidfile : ./tmp/pids/shoryuken.pid ※groupsで分けている理由 groups : group1 : concurrency : 1 queues : - [ 'a_queue' , 2 ] # 重さ2 - [ 'b_queue' , 1 ] # 重さ1 このように同じグループに複数のキューを設定している場合、 a_queue がメッセージを取得しても b_queue にメッセージがない場合はポーリング時間が終わるまで a_queue の処理は実行されないので注意が必要です。 Processing Groups · phstc/shoryuken Wiki · GitHub また a_queue と b_queue で大量にメッセージがある場合、 a_queue は b_queue の重さの2倍なので、処理の優先度も2倍になります。 Polling strategies · phstc/shoryuken Wiki · GitHub shoryuken.rb SQSの情報を記載します。 # config/initializers/shoryuken.rb # ロングポーリング Shoryuken .sqs_client_receive_message_opts = { wait_time_seconds : 20 } Shoryuken .sqs_client = Aws :: SQS :: Client .new( region : ENV [ ' AWS_REGION ' ], access_key_id : ENV [ ' AWS_ACCESS_KEY_ID ' ], secret_access_key : ENV [ ' AWS_ACCESS_SECRET_KEY ' ] ) 起動 $ bundle exec shoryuken -R -C config/shoryuken.yml ジョブ 例)決済完了時に実行するジョブ # app/jobs/purchase_complete_job.rb # 決済完了時のジョブ class PurchaseCompleteJob < ApplicationJob include Shoryuken :: Worker shoryuken_options queue : ' <キュー名> ' , auto_delete : true , body_parser : :json def perform (_sqs_msg, body) message = JSON .parse(body[ ' Message ' ]) # 保存処理 end end エンキュー エンキューも簡単にできます。 # 配送ステータスの通知ジョブにエンキュー StatusNotificationJob .perform_async( status_data : ' some status data ' ) FIFO キュー 前述した FIFO キューですが、Shoryukenでも扱うことができます。 基本通常キューと記述は同じですが、メッセージグループIDを指定することができます。 同じメッセージグループIDだと厳密な順序で、常に1件ずつ処理をします。 例えば取引のステータスを変更したい場合、ステータス変更の順序は重要だが、他の取引同士の順序は気にしない場合はメッセージグループIDに取引IDを指定したら良さそうです。 SomeJob .perform_async( csv_id : csv.id, message_group_id : < MESSAGE_GROUP_ID > ) イベント駆動は一筋縄ではいかない話 各技術の説明をしてきましたが、ここからはイベント駆動 アーキテクチャ 導入時に必ず考慮する点を書いていきます。 冪等性 同じ処理を何度実行しても同じ結果が得られる性質のことです。 冪等 - Wikipedia Amazon SQSの通常キューでは2回以上同じメッセージを取得する可能性があるので、重複してメッセージを取得することを考慮する必要があります。 今回実装した決済完了イベントでは、取引ID(冪等キー)をUnique Keyにして、DB側で制御するようにしました。 # 配送サービスの配送情報保存処理 def save! Delivery .save!(params) rescue ActiveRecord :: RecordNotUnique => e # 念の為取引ID(冪等キー)で重複していることを確認 raise e unless Delivery .exists?( order_id : params[ :order_id ]) # ログだけ残して正常終了させる Rails .logger.info( "#{ self .class } # #{ __method__ } Message: #{ e }" ) end もしくは FIFO キューを導入することでも解決が可能です。 整合性 モノレポの場合はDBのtransaction機能を利用して、失敗したら ロールバック することが可能です。しかしマイクロサービスだとそう簡単にはできません。 一般的には TCC パターンやSagaパターンを使用する必要がでてきます。 qiita.com 今回は下記理由で、 ロールバック 処理を行いませんでした。 連携するサービス数が少ないので今のところ処理がシンプル 外部からの入力事項は送信前にバリデーションされている BUYMA 本体から配送サービスに送られてくるメッセージは信頼する BUYMA 本体の取引データと配送データに不整合がないかを毎日チェックしている 現状は問題なく動作していますが、今後はもっと堅牢にするために TCC パターンやSagaパターンを導入する必要がありそうです。 最後に イベント駆動 アーキテクチャ に使用した技術と考慮するポイントを書いていきましたが、マイクロサービスは思っている以上に学習することが多く、また実際に作成して経験値を積んでいくことが大事だと実感しました。 今後も実践を経て、レガシーから脱却できるように精進していきたいです。 明日の記事の担当は検索エンジニアの伊藤さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
この記事は Enigmo Advent Calendar 2020 の15日目の記事です。 エニグモ では、2日目の記事 「デザインツールをXd→Figmaへした話 / プロトタイプ作るようになった話」 にもあるように、UIに関わるメンバーは、 Figma を使用してデザインすることが多くなりました。 私もそのうちの一人で、SketchやXDに比べて便利なことが多く、デザインの確認・共有がしやすくなったように感じています。 日頃の業務で Figma を使っていて、作ってみたい機能が出てきたため、 Figma Pluginの開発を始めてみました。 記事について 本記事では、 Figma Pluginの開発を始めるまでの作業や内容について説明します。 Figma Pluginの公式ドキュメント は大変分かりやすく書かれていますが、少し内容が古くなっている箇所もあり、そのまま進めても動作しないこともありましたので記していければと思います。 ちなみに、本記事は公開日直前に書いており、後述する作りたい プラグイン は本記事内で完成しません。すみません。 作りたいもの 日頃の業務で、デザインを作成したときにUIの設計意図や説明をコメントとして、デザイン上に添えることがよくあります。(以下イメージ) コメントする度に、「frame」を用意して、テキストを置いて、どこの部分にコメントしているのかを分かるようにする必要があり、少し手間だと感じておりました。 どこか効率化できないかと思い、 Figma Pluginの作成を試みました。 想定した プラグイン の概要は、まず選択した「Layer」の名前を取得し、 プラグイン のUI側でコメントを書いて、ボタンを押すと、コメント用の「Frame」が生成されるというものです。デザイン上のコメントは、時には邪魔になることもあるので、一括で表示・非表示なども制御できるとよいなと考えております。 『 Figma のコメント機能でやればいいじゃないか』と思われるかもしれませんが、コメントするためにモードを切り替える必要があり面倒で、また、デザインを見たメンバーが確実にコメントまで見てくれているとは限らないため、今のところはデザイン上に「frame」としてコメントを置きたいと考えております。 用意しておくもの Visual StudioCode エディタは基本的に何でも大丈夫だと思いますが、TypeScriptを使用するため公式では VSCode が推奨されています。 Node.js まだの方は以下のサイトからダウンロードしておきます TypeScript JavaScript でも開発はできますが、変数がどんなプロパティを持っているのかすぐに知ることができるため推奨されています。 Figmaデスクトップアプリ ブラウザのものではなく、デスクトップアプリが必要です。ローカルファイルを読み込みながら開発できます。 プラグイン を動かすまで Figma アプリを起動 アプリケーションメニューの「Plugins」>「Manage Plugins...」を選択します。 表示された画面の「In Development」のセクションから「+」ボタンをクリックします。 プラグイン のテンプレートを作成、ローカルへ保存 「Create a plugin」のモーダルが出てくるので、作りたい プラグイン の名前を入力し、「Continue」ボタンをクリックします。 「Choose a template」の画面に進むので、自分が作りたいテンプレートの形式を選択します。今回私はUIを伴った プラグイン を作成したいので、一番右の「With UI & browser APIs」を選択しました。 「Save as...」からローカルに保存します。保存すると、先程の「In Development」のセクションに連携されます。 VSCode でファイルを開く 7つのファイルで構成されています。それぞれの説明は、後述します。 TypeScriptの自動 コンパイル の設定 「ターミナル」>「ビルド タスクの実行...」から「 tsc :ウォッチ - sample/tsconfig. json 」を選択し、クリックします。 これで編集を加える度に自動で コンパイル が実行されますが、 TS2304 のエラーが出るので、型定義ファイルをインストールしてください。 npm install --save-dev @figma/plugin-typings これでエラーがなくなったと思います。 先程保存した プラグイン が実際に Figma アプリ上で動くようになります。 Figma で プラグイン を実行 「Plugins」> 「Development」 > 「sample」をクリックすると プラグイン が実行すると、長方形作成者が現れます。ちなみに、「Plugins」> 「Development」 > 「Open console」で見慣れたコンソールを表示することができます。 構成ファイルについて manifest. json name プラグイン の名前 id Figma から割り当てられたid api 使用するFigmaAPIのバージョン main JavaScript ファイルの 相対パス を文字列で指定 ui HTMLファイルの 相対パス を文字列で指定 詳しい説明は こちら に記載されております。 code.ts Figmaのplugin API を利用して Figma 上の操作を行うファイル 先程の長方形を作成する プラグイン でいうと、 Figma のページに長方形を作成する処理が書かれています。 figma.showUI ( __html__ ); figma.ui.onmessage = msg => { if ( msg. type === 'create-rectangles' ) { const nodes: SceneNode [] = [] ; for ( let i = 0 ; i < msg.count ; i ++) { const rect = figma.createRectangle (); rect.x = i * 150 ; rect.fills = [{ type : 'SOLID' , color: { r: 1 , g: 0.5 , b: 0 }}] ; figma.currentPage. appendChild ( rect ); nodes.push ( rect ); } figma.currentPage.selection = nodes ; figma.viewport.scrollAndZoomIntoView ( nodes ); } figma.closePlugin (); } ; code.js 上記 code.ts が コンパイル された後の姿 manifest. json で読み込まれ、実行されるファイル ui.html プラグイン 側のUIを担うファイル 先程の長方形を作成する プラグイン でいうと、長方形の数を入力する プラグイン 側のUI部分になります。 < h2 > Rectangle Creator </ h2 > < p > Count: < input id = "count" value = "5" ></ p > < button id = "create" > Create </ button > < button id = "cancel" > Cancel </ button > < script > document .getElementById ( 'create' ) .onclick = () => { const textbox = document .getElementById ( 'count' ) ; const count = parseInt ( textbox.value, 10) ; parent .postMessage ( { pluginMessage: { type: 'create-rectangles' , count } } , '*' ) } document .getElementById ( 'cancel' ) .onclick = () => { parent .postMessage ( { pluginMessage: { type: 'cancel' } } , '*' ) } </ script > HTMLファイルになっておりますが、 <script> タグで JavaScript のコードが埋め込まれています。 このファイル側、 つまりUI側では Figma の操作を直接行うことができず 、Web Messaging API のpostMessageを介して、 code.ts とお互いにやり取りすることで連携しています。 ※ その他にも構成ファイルには、 package.json tsconfig.json README.md があります。 Webpack + Reactの導入 せっかくなので、Reactを導入して開発していきたいと思います。 導入の仕方は自由ですが、今回は公式ドキュメントに沿って進めていきます。 インストール 公式のドキュメントには以下のインストール 行うように書かれていますが、 npm install --save-dev css -loader html-webpack-inline-source-plugin@beta html-webpack-plugin style-loader ts-loader typescript url-loader webpack webpack- cli この通りにインストールしてしまうと動かないため、 Figmaが提供しているReactのサンプル の依存関係を参考にして、インストールします。 " dependencies ": { " @types/react ": " ^16.8.23 ", " @types/react ": " ^16.8.23 ", " @types/react-dom ": " ^16.8.5 ", " css-loader ": " ^3.1.0 ", " html-webpack-inline-source-plugin ": " 0.0.10 ", " html-webpack-plugin ": " ^3.2.0 ", " react ": " ^16.8.6 ", " react-dom ": " ^16.8.6 ", " style-loader ": " ^0.23.1 ", " ts-loader ": " ^6.0.4 ", " typescript ": " ^4.0.3 ", " url-loader ": " ^2.1.0 ", " webpack ": " ^4.38.0 ", " webpack-cli ": " ^3.3.6 " } ファイル作成 ライブラリのインストールが終わったら、構成ファイルを変更します。新たに以下のファイルを作成し、中身は サンプル を参考にします。 src/code.ts src/ui.html src/ui. tsx src/ui. css src/logo. svg webpack.config.js ファイルの変更 manifest.json の ui と main を以下のように書き換えます。 { ... " main ": " dist/code.js ", " ui ": " dist/ui.html ", ... } tsconfig.json を以下のように書き換えます。 { " compilerOptions ": { " target ": " es6 ", " jsx ": " react ", " typeRoots ": [ " ./node_modules/@types ", " ./node_modules/@figma " ] } } ファイルを削除 先程まで使用していた code.ts , ui.html ファイルは不要になるので削除します。 これで環境が整いました。 Figma デスクトップアプリから実行すると、 プラグイン が実行されるようになりました。 プラグイン のUIデザイン 最後になりましたが、 プラグイン のUIは、 Figma Components が参考になりそうでした。 必ずしも Figma に沿ったUIである必要はなさそうですが、 Plugin Review Guidelines でも Figma に沿ったデザインが推奨されています。 We highly recommend matching your plugin to Figma 's UI so we can create a seamless experience for our users. 終わりに 今回は Figma Plugin作成の準備についてまとめました。 デザイナーであっても自力で作成できる範囲だと思いましたので、これから冒頭で述べた プラグイン を作っていければと思います。 明日の記事の担当は、サーバーサイドエンジニアの @hokita さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
この記事は Enigmo Advent Calendar 2020 の14日目の記事です。 はじめに こんにちは、 エニグモ 嘉松です。 簡単な自己紹介ですが、 BUYMA のプロモーションや マーケティング を行っている事業部に所属して、その中のデータ活用推進室という部署で会社のデータ活用の推進や マーケティング ・オートメーションツール(MAツール)を活用した販促支援、 CRM などを担当しています。(データ活用推進室、長らく私一人部署だったのですが、先月1名増えて2名体制になりました!) 目次 はじめに 目次 背景 日付および時刻関連のデータ型 SQL Server BigQuery Redshift タイムゾーンとは? データ型まとめ 現在日時(日付と時間)の取得方法 SQL Server GETDATE関数 BigQuery CURRENT_TIMESTAMP関数 Redshift SYSDATE関数 GETDATE関数 現在日付の取得方法 SQL Server BigQuery CURRENT_DATE関数 Redshift CURRENT_DATE関数 現在日時の取得方法まとめ 日付型 → 文字型 SQL Server 年月日(YYYY/MM/DD形式) 年月日(YYYYMMDD形式) 年月日時分秒(yyyy-mm-dd hh:mi:ss.mmm (24h)) 年月日時分秒(yyyy-mm-ddThh:mi:ss.mmm形式(ISO8601標準)) BigQuery DATE型 TIMESTAMP型 Redshift 文字型 → 日付型 SQL Server datetime型 BigQuery DATE型 TIMESTAMP型 Redshift DATE型 TIMESTAMP型 最後に 背景 エニグモ のデータ活用の大きな特徴として、エンジニアに限らず、マーケターや マーチャンダイザー (MD)、カスタマーサポートや役員まで、多くの社員、ほとんどの社員と言っても過言では無いくらいの人が自分で SQL を叩いてデータを見る、分析するという文化、カルチャーが根付いているということが言えると思います。 また、データ活用基盤の整備も積極的に進めており、 クラウド で提供されているビックデータ向けのデータベースをデータレイクやデータウェアハウス(DWH)として利用しています。 このように複数のデータベースを活用してく中で出てくる問題点が、 SQL の シンタックス の違いです。 特に エニグモ ではエンジニアでは無いユーザもたくさんいるので、 SQL の作成に多くの時間がとられてしまうと、本来の業務へも影響がでてきてしまいますし、データ活用は停滞してしまいます。 そこで、この記事では Microsoft が提供している SQLServer 、 Google が提供しているフルマネージド型分析データウェアハウスであるBigQuery、 Amazon Web Services ( AWS )の クラウド 型データウェアハウスであるRedshiftの3製品を対象として、特に混乱するであろう日付および時刻関連のデータ型について整理することで、今後のリファレンスになればと思っています。 日付および時刻関連のデータ型 まず、ここでは各データベースの日付および時刻関連のデータ型(の代表的なもの)を列挙します。 ※データ型の表記(大文字小文字)、説明の内容については各データベースのマニュアルにおおよそ準拠しています。 SQL Server データ型 説明 タイムゾーン date 日付型 なし datetime 日時型( タイムゾーン なし) なし datetimeoffset 日時型( タイムゾーン あり) あり BigQuery データ型 説明 タイムゾーン DATE 日付型 なし DATETIME 日時型 なし TIMESTAMP タイムスタンプ型 あり Redshift データ型 説明 タイムゾーン DATE カレンダー日付 (年、月、日) なし TIMESTAMP 日付と時刻 ( タイムゾーン なし) なし TIMESTAMPTZ 日付と時刻 ( タイムゾーン あり) あり タイムゾーン とは? タイムゾーン について言及すると、それだけで1本の記事になるくらいなので、簡単に説明します。 データーベースにおける タイムゾーン のあり・なしとは、標準時間を UTC とするか、それとも個々のデータベースで決めるか、ということです。 タイムゾーン ありのデータ型を使う場合は、当然、データを格納する時にも タイムゾーン を指定してデータを格納する必要があります。 また、 タイムゾーン なしのデータ型を使う場合は、そのデーターベースにはどの タイムゾーン でデータが格納されているかを、意識して使う必要があります。 例えば日 本の時間 で格納したデータを、ニューヨークの時間帯で表示させるには、時間を14時間戻してあげるといったことを意識的に行う必要があります。 いずれにしても、ひとつのデータベースで時差のある地域の時間を扱う場合は、時差を意識することからは逃れられません。基準となる時間を UTC にするのか、どうかの違いです。 逆に日本時間だけで良いシステムであれば、扱う時間は常に日本時間なので、 タイムゾーン なしのデータ型を使うことで、 タイムゾーン を意識する必要がなくなります。 データ型まとめ 日付型 日時型( タイムゾーン 無し) 日時型( タイムゾーン 有り) SQLServer date datetime datetimeoffset BigQuery DATE DATETIME TIMESTAMP Redshift DATE TIMESTAMP TIMESTAMPTZ 日付型は3データベースとも DATE で分かりやすいですね。 日時型( タイムゾーン 無し)は SQLServer とBigQueryが DATETIME なのに対して、Redshiftが TIMESTAMP 。 日時型( タイムゾーン 有り)は全てのデーターベースで異なります。 更に TIMESTAMP はRedshiftでは日時型( タイムゾーン 有り)なのに対して、BigQueryでは日時型( タイムゾーン 無し)となっています。 この時点で既にややこしくなってますね。 現在日時(日付と時間)の取得方法 次に、それぞれのデータベースで現在の日時(日付と時間)を取得する関数を見ていきます。 ここに挙げた関数以外もありますが、よく使う(であろう)ものを列挙しています。 SQL Server GETDATE関数 戻り値の型:datetime SELECT GETDATE() ; ------------ 2020-12-09 08:20:17.645 BigQuery CURRENT_TIMESTAMP関数 戻り値の型:TIMESTAMP SELECT CURRENT_TIMESTAMP() ; ------------ 2020-12-10 08:07:47.222776 UTC 括弧は省略可能です。 UTC で表示されます。 日本時間( JST )で表示させたい場合は以降の「日付型 → 文字型」を参照ください。 Redshift SYSDATE関数 戻り値の型:TIMESTAMP select sysdate ; ------------ 2020-12-09T08:20:17.645728 GETDATE関数 戻り値の型:TIMESTAMP select getdate() ; ------------ 2020-12-09T08:20:17.645728 どちらもデフォルトでは UTC が表示されるので、日本時間を表示したい場合はセッションのタイム ゾーン(デフォルトでは UTC )を設定してあげる必要があります。 set timezone = 'Asia/Tokyo'; select sysdate ; ------------ 2020-12-09T17:20:17.645728   set timezone = 'Asia/Tokyo'; select getdate() ; ------------ 2020-12-09T17:20:17.645728 現在日付の取得方法 SQL Server SQL Server には単体で日付を取得する関数が無いので、 GETDATE() で現在の日にち時刻を取得した後に、 CONVERT を使ってdate型に変換してあげる必要があります。 SELECT CONVERT(date, GETDATE()) ; ------------ 2020-12-09 BigQuery CURRENT_DATE関数 戻り値の型:DATE SELECT CURRENT_DATE() ; ------------ 2020-12-09 引数に何もして指定しないと UTC の日にちが返ってくるので、日本時間での日にちを取得する場合は、引数に タイムゾーン を指定してあげます。ここ注意ですね。 SELECT CURRENT_DATE("Asia/Tokyo") ; ------------ 2020-12-09 Redshift CURRENT_DATE関数 戻り値の型:DATE select current_date ; ------------ 2020-12-09 現在日時の取得方法まとめ SQL Server BigQuery Redshift 日時 GETDATE() CURRENT_DATETIME() sydate getdate() 日付 なし CURRENT_DATE() current_date 日付型 → 文字型 日付型のデータを文字列に変換する方法について記載します。 SQL Server 年月日(YYYY/MM/DD形式) SELECT CONVERT(nvarchar, getdate(), 111) ; ------------ 2020/12/10 年月日(YYYYMMDD形式) SELECT CONVERT(nvarchar, getdate(), 112) ; ------------ 20201210 年月日時分秒(yyyy-mm-dd hh:mi:ss.mmm (24h)) SELECT CONVERT(nvarchar, getdate(), 21) ; ------------ 2020-12-10 16:34:37.837 年月日時分秒(yyyy-mm-ddThh:mi:ss.mmm形式(ISO8601標準)) SELECT CONVERT(nvarchar, getdate(), 126) ; ------------ 2020-12-10T16:30:05.690 BigQuery DATE型 FORMAT_DATE(format_string, date_expr) 指定された format_string (形式設定要素)に従って date_expr をフォーマットします。 DATE型でサポートされる形式設定要素 形式設定要素 説明 %Y 10 進数として表示される、世紀を含む年。 %y 10 進数(00-99)として表示される年。世紀は含みません。 %m 0 進数として表示される月(01~12)。 %d 10 進数として表示される、月内の日付(01~31)。 %F %Y-%m-%d 形式の日付。 年月日(YYYYMMDD形式) SELECT FORMAT_DATE("%Y%m%d", CURRENT_DATE()) ; ------------ 20201210 年月日(YYYY-MM-DD形式) SELECT FORMAT_DATE("%F", CURRENT_DATE()) ; ------------ 2020-12-10 TIMESTAMP型 FORMAT_TIMESTAMP(format_string, timestamp[, timezone]) 指定された format_string (形式設定要素)に従って timestamp をフォーマットします。 タイムゾーン を指定すると指定した タイムゾーン に変換されて表示されます。 タイムゾーン名 TIMESTAMP型でサポートされる形式設定要素 形式設定要素 説明 %Y 10 進数として表示される、世紀を含む年。 %y 10 進数(00-99)として表示される年。世紀は含みません。 %m 10 進数として表示される月(01~12)。 %d 10 進数として表示される、月内の日付(01~31)。 %H 10 進数で表示される時間(24 時間制)(00~23)。 %M 10 進数として表示される分(00~59)。 %S 10 進数として表示される秒(00~60)。 %F %Y-%m-%d 形式の日付。 %T %H:%M:%S 形式の時刻。 %Z タイムゾーン の名前。 %z 必要に応じて +HHMM または -HHMM の形式で示される グリニッジ 子午線からのオフセット。 年月日時分秒(yyyy-mm-dd hh:mi:ss.mmm (24h)) SELECT FORMAT_TIMESTAMP("%Y-%m-%d %H:%M:%S", CURRENT_TIMESTAMP(), "Asia/Tokyo") ; ------------ 2020-12-10 17:13:13 Redshift TO_CHAR (timestamp_expression, 'format') 日時形式の文字列 形式設定要素 説明 YYYY 4 桁の年数 MM 月番号 (01~12) DD 日にちを数字表示 (01–31) HH24 時 (24 時間制、00–23) MI 分 (00–59) SS 秒 (00–59) 年月日時分秒(yyyy-mm-dd hh:mi:ss.mmm (24h)) UTC select to_char(sysdate, 'YYYY-MM-DD HH24:MI:SS') ; ------------ 2020-12-10 08:13:13 年月日時分秒(yyyy-mm-dd hh:mi:ss.mmm (24h)) JTC select ,to_char(convert_timezone('Asia/Tokyo', sysdate), 'YYYY-MM-DD HH24:MI:SS') ; ------------ 2020-12-10 17:13:13 文字型 → 日付型 SQL Server datetime型 日付だけ指定した場合は時分秒は0時0分0秒となります。 SELECT CONVERT(datetime, '2020/12/10') ; ------------ 2020-12-10T00:00:00 日付のスラッシュ(/)は省略することもできます。 SELECT CONVERT(datetime, '20201210') ; ------------ 2020-12-10T00:00:00 時分秒を指定したい場合は日付の後にスペースを開けて、 時:分:秒 を付けます。 SELECT CONVERT(datetime, '2020/12/10 12:15:30') ; ------------ 2020-12-10T12:15:30 BigQuery DATE型 CAST を使います。 年月日は - ハイフンで区切ります。 2020/12/10 のように / スラッシュで区切ったり、 20201210 のように区切らない場合はエラーになります。 SELECT CAST('2020-12-10' AS DATE) ; ------------ 2020-12-10 TIMESTAMP型 日付だけ指定した場合は時分秒は0時0分0秒となります。 また、 タイムゾーン は UTC になります。 SELECT CAST('2020-12-10' AS TIMESTAMP) ; ------------ 2020-12-10 00:00:00 UTC 時分秒を指定したい場合は日付の後にスペースを開けて、 時:分:秒 を付けます。 SELECT CAST('2020-12-10 12:15:30' AS TIMESTAMP) ; ------------ 2020-12-10 12:15:30 UTC タイムゾーン を指定したい場合は +09 のように UTC からの時差を指定します。 SELECT CAST('2020-12-10 12:15:30+09' AS TIMESTAMP) ; ------------ 2020-03-10 12:15:30 UTC Redshift DATE型 TO_DATE (string, format) 引数には、変換したい文字列とそのフォーマットを指定します。 SELECT TO_DATE('2020/12/10', 'YYYY/MM/DD') ; ------------ 2020-12-10 フォーマットの方法によって変換したい文字列の形式を指定できます。 SELECT TO_DATE('2020-12-10', 'YYYY-MM-DD') ; ------------ 2020-12-10 こんなことでも大丈夫です。 SELECT TO_DATE('2020###12$$$10', 'YYYY###MM$$$DD') ; ------------ 2020-12-10 CASTを使うこともできます。 SELECT CAST('2020-12-10' AS DATE) ; ------------ 2020-12-10 TIMESTAMP型 日付だけ指定した場合は時分秒は0時0分0秒となります。 SELECT CAST('2020-12-10' AS TIMESTAMP) ; ------------ 2020-12-10 00:00:00 年月日の区切りは / でも大丈夫です。 SELECT CAST('2020/12/10' AS TIMESTAMP) ; ------------ 2020-12-10 00:00:00 区切り文字がなくても大丈夫です。 SELECT CAST('20201210' AS TIMESTAMP) ; ------------ 2020-12-10 00:00:00 時分秒を指定したい場合は日付の後にスペースを開けて、 時:分:秒 を付けます。 SELECT CAST('2020-12-10 12:15:30' AS TIMESTAMP) ; ------------ 2020-12-10 12:15:30 最後に この記事では、 SQL Server 、BigQuery、Redshiftの3つのデーターベースを対象に、日付および時刻関連のデータ型についてまとめました。 日頃、私は上記のデータベースを使い分けている、それもおおよそ均等に使っているような状況なので、特に日付型の関数についてはよく迷ったりしています。 何度 bigquery 日付 文字列 変換 でググったことか。 今回、このように整理することで、迷ったときはこの記事を参照することで、少しでも生産性を高められたらと思っています。 記載した SQL については、実際に実行した上で確認していますが、データーベースのバージョンの違いなどによってエラーになったり、そもそも間違っていたりする可能性もあるので、その場合はコメントなどに記載いただければ、修正や補足など入れていきたいと思っています。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。Enigmoインフラエンジニアの夏目です。 この記事は Enigmo Advent Calendar 2020 の13日目の記事です。 なんだか競馬関連のエントリがいっぱいですが、弊社の主要サービスは競馬予想サイトではありませんので誤解なきよう。僕は競馬のことはさっぱりわからないのですが、先月末の ジャパンカップ は大変熱いレース展開でしたね。着順自体はまったく面白みがなく収支マイナスになってしまいましたが。 さておき。 1年前 と同様、今年も Kubernetes クラスタ 運用に翻弄される日々を過ごしておりまして、今日の記事はそんな Kubernetes ...というか Amazon EKS クラスタ に関するお話です。 Kubernetes のリリースサイクルに乗り遅れるな 皆さんご存知の通り Kubernetes のマイナーバージョンはおよそ3ヶ月ごとにリリースされ、各マイナーバージョンは最新バージョンとの差異が3以上になった時点でコミュニティのサポート対象外となります。 https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md#release-versioning つまり、同一バージョンを1年以上利用することはほぼ不可能に近く、どうしても1年のうち最低でも1回はアップグレード作業が必要となります。これは Kubernetes を本番環境で利用する上で避けることができない、インフラエンジニアの頭を悩ませる問題のひとつです。 (注: 1.19以降は1年間のサポートがアナウンスされていますが 、バージョン差異は4つも進んでしまうのでどのみちアップグレードしないわけにはいきません) エニグモ の一部サービスでは Amazon EKS クラスタ を利用しており、 春先に1.15がリリースされた タイミングで1.13からアップグレードを実施しました。 EKSは Kubernetes 本家のリリースからおよそ半年遅れでリリースが行われ、本家よりも3ヶ月長い1年程度のサポートが保証されています。 そのため、1.15は2021年3月…よりも少し先の5月までがサポート予定となっており、年内はさらなるアップグレードは当面必要ないかな、と高を括っていました。 というのも、EKSは本家への追従に時間がかかりなかなかリリースされないこともあり、サポート期間はさておき年内に1.17や1.18がリリースされるかどうかも疑わしいものだ、と半ば AWS 開発チームの対応スピードを侮っていました。 ところが、1.15のリリースから2ヶ月も経たないうちになんと 1.16がリリースされました 。3ヶ月ごとじゃないじゃん!話が違うよ!と憤慨しながら GithubのAWS Container Roadmap を眺めていると、こんな頼もしいコメントが寄せられているではありませんか。 https://github.com/aws/containers-roadmap/issues/487#issuecomment-597444626 but one of priorities for EKS this year is to reduce the gap between upstream releases and EKS support, which will require a temporary release schedule that is sooner than every 90 days. ということで、本家リリースから10ヶ月も経ってからようやく公開されたEKS 1.15はたった1ヶ月半で旧バージョン扱いになってしまったのでした。ひどい。 1.16へアップグレードする前に さて、本家で1.16がリリースされてからもう1年以上経過しているため、GKEなどで常に最新バージョンの Kubernetes を利用されている方には遠い昔のことのように思われるかもしれませんが、1.16では一部の API が非推奨となりました。 https://github.com/kubernetes/kubernetes/blob/release-1.16/CHANGELOG/CHANGELOG-1.16.md#deprecations-and-removals 非推奨となったものの中でも最も広範かつ影響が大きいのは、Deployment , DaemonSet , ReplicaSet リソースが対象となる apiVersion:extensions/v1beta1 グループです。 これらのリソースを apps/v1 へ変更するために、現在 クラスタ で稼働しているアプリケーションを確認してmanifestを修正して……といった作業 工数 を考えると、1.15のサポート期間終了直前に慌てて1.16へアップグレードすることはあまり現実的ではないと判断し、早々に1.16へのアップグレード準備作業を始めることとなりました。 API バージョン変更対応 対象リソースの洗い出し 1年前の記事でご紹介したように、アプリケーションのmanifestは基本的にGitで管理しているので、 apiVersion:extensons/v1beta1 のリソースの有無はGit リポジトリ を確認すればよいのですが、アプリケーション以外の一部のモジュールは Github リポジトリ のkustomizationファイルを直接参照してデプロイしているため、 kustomize build コマンドを実行しないとmanifestを確認することができません。 すべてのモジュールを確認して回るのも手間だしどうしたものか、と思っていたところ kube-no-trouble:kubent という便利な スクリプト を見つけました。 https://github.com/doitintl/kube-no-trouble $./kubent 6:25PM INF >>> Kube No Trouble `kubent` <<< 6:25PM INF Initializing collectors and retrieving data 6:25PM INF Retrieved 103 resources from collector name=Cluster 6:25PM INF Retrieved 132 resources from collector name="Helm v2" 6:25PM INF Retrieved 0 resources from collector name="Helm v3" 6:25PM INF Loaded ruleset name=deprecated-1-16.rego 6:25PM INF Loaded ruleset name=deprecated-1-20.rego __________________________________________________________________________________________ >>> 1.16 Deprecated APIs <<< ------------------------------------------------------------------------------------------ KIND NAMESPACE NAME API_VERSION Deployment default nginx-deployment-old apps/v1beta1 Deployment kube-system event-exporter-v0.2.5 apps/v1beta1 Deployment kube-system k8s-snapshots extensions/v1beta1 Deployment kube-system kube-dns extensions/v1beta1 __________________________________________________________________________________________ >>> 1.20 Deprecated APIs <<< ------------------------------------------------------------------------------------------ KIND NAMESPACE NAME API_VERSION Ingress default test-ingress extensions/v1beta1 この スクリプト で apiVerison:extensions/v1beta1 のリソースを洗い出して順次 API バージョンの変更を実施しました。 安全にリソース API バージョンを変更するには リソースの API バージョン変更と言っても、単純に apiVersion:apps/v1 へ変更するだけではありません。 spec.selector フィールドの追加も必要なのですが、このフィールドはimmutableとして定義されているため、既存のリソースに追加しようとしても以下のようなエラーが出力されてしまいます。 The Deployment "sample-application" is invalid: spec.selector: Invalid value : v1.LabelSelector{MatchLabels:map[string]string{"app":"sample-application", "app. kubernetes .io/name":"sample-application"}, MatchExpressions:[]v1.LabelSelectorRequirement( nil )}: field is immutable このエラーを無視して kubectl apply --force コマンドで強制的にmanifestの変更を適用することはできるものの、整合性が取れないため既存のリソースは一度完全に削除されてから、 apps/v1 のリソースが新たに作成される形になります。つまり、稼働Pod数が一時的に完全に0になってしまうのです。 Pod数が0になってしまえば当然サービスに影響が生じてしまうため、以下のようなフローで API バージョンを変更しました。 既存のリソースと同一構成のmanifestを作成し、 metadata.name のみ変更して クラスタ へデプロイ 例) Deployment: sample-app を複製し、 Deployment: sample-app-temp を作成 複製したリソースが Service に紐づき、 トラフィック が振り分けられていることを確認 kubectl get endpoints コマンドで、ServiceとDeploymentの紐付けを確認できます 既存のリソースに対して API バージョン変更及び spec.selector を追加するmanifestを強制的に適用 既存リソースのDeploymentが削除され、 apps/v1 で再作成されてアプリケーションが正常に動作していることを確認 一時的に作成したリソースを削除 少々泥臭いですが、ブルーグリーンデプロイメントの亜種のようなやりかたですね。サービスに影響なく安全にリリースできる安心感はありますが、同様の要件が発生した際に都度こういった対応をするのも手間なので、今後は Argo Rollout などのモジュールも試してみたいところです。 ノードグループのマネージド化 非推奨 API バージョンのリソースは一新できたため、あとはコン トロール プレーンとノードグループをそれぞれ1.16へアップグレードするだけです。 ただ、これまで利用していたノードグループはEKS 1.13の頃に AWS ドキュメントに従ってEC2 LaunchTemplate と AutoScalingGroupで作成していたため、実際にアップグレードしようとすると、以下のように複数フェーズで対応をする必要がありました。 新規バージョンのAMIを利用するLaunchTemplate, AutoScalingGroupリソースを作成し、コン トロール プレーンへ紐付け 既存のノードグループにTaintを付与し、Drainを実行してPodを新規ノードグループへ退避させる 既存のノードグループを削除 さきほどの API バージョン変更時と同じような作業ですが、これをCloudFormationで対応しようとすると、3度もスタックの変更セットを適用するはめになり大変面倒です。 このため、EKS 1.15へアップグレードしたタイミングでマネージドノードグループへ移行しようとしたのですが、リリース当初のマネージドノードグループはSecurityGroupの割当てをすることができず、RDSやElastiCache, ALBなどの AWS リソースとPod間の疎通設定ができないため踏み切ることができませんでした。 ノード単位でSecurityGroupを割り当てなくても SecurityGroup for Pod を利用すれば良いでしょ?と思っても、対応 クラスタ バージョンはEKS 1.17以降のため使うこともできない、といった具合で数カ月間スタック状態でした。 それが この夏ようやくLaunchTemplateに対応 し、SecurityGroupを自由に割り当てられるようになったため、1.16へのアップグレード前にマネージドノードグループへ移行することにしました。 移行作業は前述のアップグレードフローとほぼ同様で、マネージドノードグループを新規作成して旧ノードグループからPodを退避させたうえで旧ノードグループを削除する、という流れですんなり終わりました。 これでアップグレード作業もスムーズに……と思いきや、マネージドノードグループに移行したタイミングでちょっとしたトラブルが発生しました。 Podが停止できない! マネージドノードグループを作成し既存のノードグループからPodを移行してから2,3日経ったところで、唐突にPodが再起動を繰り返したり、Podが正常に停止できずにいつまでも残り続けたりと不安定な状態になり、ノードのCPUやメモリが高負荷となって クラスタ 上からもノードが利用できなくなってしまいました。 厄介なことにPodの移行が済んだことで既存のノードグループは削除済み、という状況のためPodを切り戻すこともできず、騙し騙しPodを動かしていたところ以下のバグを踏んでいたことが判明しました。 Pods stuck in terminating state after AMI amazon-eks-node-1.16.15-20201112 つまるところバグが含まれるAMIでマネージドノードグループを作成したことが原因だったため、慌ててCloudFormationで旧バージョンのAMIに変更しようとしたところ、今度は以下のようなエラーが。 Requested Nodegroup release version 1.15.11-20201007 is invalid. Allowed release version is 1.15.12-20201112 (Service: AmazonEKS; Status Code: 400; Error Code: InvalidParameterException; Request ID: - - - - ; Proxy: null) なんとマネージドノードグループは 最新バージョンのAMIしか利用できない という仕様のため、最新バージョンのAMIにバグがあるとどうすることもできないのです。詰んだ。 結局そうこうしているうちに修正バージョンのAMIがリリースされたため、マネージドノードグループのAMIバージョンを変更して一件落着……と思いきや、そうは問屋がおろしません。 前述したとおり、 Podが正常に停止できない ということは、すなわちマネージドノードグループの更新処理における Tainの付与 → Drainの実行 → PodのEviction の流れで、最終的にPodのEvictionに失敗してしまい、ノードグループの更新処理も失敗してしまうのです。そんな。ひどい。ひどすぎる。 二重に詰んだ状況になってしまったため、最終的に取った手段は CloudFormationでマネージドノードグループの更新を実行 kubectl get pods --all-namespaces でPodの稼働状況を注視 Terminating のまま一定時間変化がないPodを見つけたら kubectl delete pods <pod name> --grace-period=0 --force コマンドで殺して回る という、いったいこれのどこが マネージド なんですか?と聞きたくなるような対応をする羽目になりました。 いたずらに過去バージョンのAMIを使うことで不要なトラブルが発生することを防ぐ、という目的であれば最新バージョンのAMIしか利用できないという方針もわからないでもないですが、じゃあちゃんと動くものをリリースしてくれ……という気持ちでいっぱいです。 バージョンスキップができない! 前記した問題が解消し、ではKubernetesnのバージョンアップをしましょうということでコン トロール プレーンをEKS 1.16へアップグレードしました。 ここでマネージドノードグループも1.16へアップグレードすると、最新バージョンであるEKS 1.18まで3回も更新処理が必要となり、都度PodのEvictionが発生します。サービスに直接影響が出ないようReplicaSetで 冗長化 はしているものの、そう何度も実行したいタイプの作業ではありません。 幸い、 コントロールプレーンとノードグループ間のバージョン差異は2バージョンまで許容される ため、コン トロール プレーンをEKS 1.17へアップグレードし、ノードグループは1.15から1.17へスキップさせようと考えました。 さきほど1.16へアップグレードしたばかりのコン トロール プレーンにCloudFormationで再度アップグレード処理を実行しようとしたところ、以下のようなエラーが発生しました。 Update failed because of Nodegroups EKSNodeGroup- ,EKSNodeGroup- ,EKSNodeGroup- must be updated to match cluster version 1.16 before updating cluster version (Service: AmazonEKS; Status Code: 400; Error Code: InvalidParameterException; Request ID: - - - - - ; Proxy: null) なるほどなるほど、コン トロール プレーンとマネージドノードグループのバージョンがね、一致してないから クラスタ のアップグレードはできませんと。なるほどなるほど。えっ、なんで?????非マネージドノードグループを使っているときは1.13から1.15へアップグレードできたのに?なんで???? といった具合で理屈はわかるものの、ドキュメントのどこにも書いていない制約にひっかかり、エラーの内容からするとマネージドノードを使っている以上は避けられないようなので渋々マネージドノードもアップグレードをすることになりました。仕方がないこととはいえ、やはり少々納得がいっていません。 アップグレードをイベント化しないために こんな調子で、ほぼマネージドなはずの Amazon EKSのアップグレード作業にずいぶんと 工数 をかける形になってしまいました。 ただ、 Amazon EKSないし Kubernetes を利用している限り、 クラスタ 本体はもちろんモジュール類( Ingress Controllerなど)のアップグレードは常に意識し続けなければなりません。安定稼働しているからといってしばらく放置していると、唐突に破壊的変更を含むリリースがアナウンスされることも珍しくありません。 四半期ごとや月次のタイミングなどで各種モジュールの最新リリースをチェックし、定期的にアップグレード作業を実施することで、サービスへの影響を最小限にして運用作業を行う方法を模索していけたら良いなと考えています。 明日の記事の担当は ハイア マチュア トレイルランナー の 嘉松 さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、ディレクターの神吉です! この記事は Enigmo Advent Calendar 2020 の12日目の記事です。 エニグモ では様々なプロジェクトに関わることが多いですが、自社サービスのプロジェクトを推進する上で大事にしていることを書いていきたいと思います。 今回はプロジェクトマネジメントの体系的なテクニックの話ではなく、プロジェクトを推進する上でのマインド面中心の記事になります。 またクライアントワークのプロジェクトとは違う部分もあるかと思います。 今思っていることや感じていることなので、今後変わっていくかもしれないのでそこはご了承ください。 そもそもプロジェクトとは? プロジェクトって何か大変そう、難しそうみたいイメージがあるかもしれませんがプロジェクトの定義を Wikipedia で確認してみました。 プロジェクトマネジメント協会が制定している PMBOK (第5版)の定義では、「プロジェクトとは、独自のプロダクト、サービス、所産を創造するために実施する有期性のある業務」とされている。つまり、会社などの通常業務や、継続的な運用管理、あるいは改善活動などは、特に開始と終了が定義されていないので、「プロジェクト」とは呼ばない。ただし、特定の期限までに特定の建築を行う、製品を開発する、システムを構築する、などは個々のプロジェクトになりうる。 独自の目標がある、期限がある業務のことなのかなと思います。 いつもやっているルーティンワークは該当しないことになります。 今までやったことない業務でスケジュールも決まっていて多部署、または社外も巻き込んだ大きなプロジェクトを推進するとなるとなかなか大変です。 そんな中大事にしていることは以下になります。 大事にしていること 1. どうすべきか常に考える。 AなのかBなのか選択を迫られることが多いです。 また関係者全員が100%満足するような決定ができないことも多く、バランスをとることも必要とされます。 自分だけで判断できないことも多くありますが、どうすべきかどうあるべきか常に検討し積み重ね、小さなことでも丸投げしないで一つ一つ考えることは大事です。 2. 自分ごと化する。 他に企画者がいる場合でも自分の企画ぐらい真剣に考えることは必要だと思います。 これをやらないと検討事項も浅くなり、関係者と話す時につじつまが合わなくなったり情熱を伝えることができません。 かなり厳しい状態でプロジェクトに アサイ ンされることもありますがそれをどうするかが腕のみせどころだと思います。 私自身、最初は自分ごと化できずに苦しんでしまうこともよくありました。。 3. 逃げないこと。 逃げないこと、あきらめないことは非常に大事です。 プロジェクトへの情熱が失われると一気に物事が進まなくなります。 あー失敗したーと思うこともありますがだいたい大丈夫です。 プロジェクトは日々の積み重ねなので地道にやるしかないです。 4.最前線にいってみる。 何が課題なのか人から聞いたり報告書を見たりするより、実際に経験してみるほうが良いと思います。 意外と聞いていたこととは違う課題でつまずいていたり、すごく重要なことを汲み取れていないこともあります。 5.他のプロジェクトにも協力する。 人が進めているプロジェクトにも協力することは大事なことです。 自分以外に情熱をもって行動してくれる人がいると諦めずプロジェクトを進めることができたりもします。 いつか自分が大変な時に助けてくれるかもしれません。 6.細かいところも覗いてみる。 開発、デザイン、データ分析、ユーザからの声 などなど細かいところも覗いたり、また人にまかせていた業務もたまには自分でやってみたりしています。 やっぱり業務の基本的なところは大事。 地味で細かいところも多いですがこうしたところ見ていると何かあった時の瞬発力につながると思っています。 私の周りではマネジメント層の方でも細かいタスクを大事にしている方も多いので尊敬です! 7.よく分からなくなったらとりあえず寝る。疲れてきたらとりあえず寝る。 いろいろ考え過ぎてどうして良いか分からなかった時は寝ましょう。 寝てみると意外と頭がすっきりして解決することも多いです。 プロジェクトは長期間になることもあるので持久力も大事です。 いろいろ上げてみましたが、キリがなさそうなのでこれぐらいにしておこうと思います。 私も最初は分からないことだらけで失敗ばかりでしたが(今でもまだまだ勉強することばかりですが。。)いろんな経験の積み重ねで少しずつプロジェクトを推進していけるようになってきたかなーと思っています。 今までないものを形にするのは非常に楽しいことなので何か新しくやりたいことがある方は失敗を恐れずぜひそのプロジェクトに挑戦してみてください!! 明日の記事の担当はインフラエンジニアの夏目さんです。宜しくお願いします! 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは、サーバーサイドエンジニアの Steven です。 この記事は Enigmo Advent Calendar 2020 の 11 日目の記事です。 抽象化という単語とその議論をそれほど目にすることがありませんが、設計においては極めて重要な概念だと思いますので、ここで抽象化は何を指すのか、何のためのものなのか、どうやるのかを説明してみます。 ソフトウェア・エンジニアリングとは それが明確となっていないと、どうして抽象化が必要なのかは曖昧となってしまうこともあるかと思いますので、まずは方針にしていることについて語ります。 解釈は複数あると思いますが、一つの文章で表すと、ソフトウェア・エンジニアリングとは人間のア イデア を アルゴリズム に変換することだと思います。 人間の観点で、不確定で無限とも取れるア イデア を、限り有る計算関数の組み合わせで有限なものに変えるとも取れます。 決まった特定な目的を果たすために、有限なものだけを使って何かを作ることから、問題解決とも取れると思います。レゴ作りやパズル解決という比喩は気に入ってます。 以上の説明のキーワードは「変換」と「問題解決」です。 一つ一つ個別なものと捉えると、ウェブ業界の通常の仕事では実に難しい問題はそうそうないと思います。業界として今まで見たことのない新しい アルゴリズム の発明はまず必要ないです。 ただし、仕事で求められるのは人間の言葉で表現される高レベルな問題の解決がほとんどで、その一つの大きな問題を解けるにはお互いに影響し合うたくさんな小さな問題を同時に解決しないといけないです。 なので、そういう問題は本質的に難しいというより、複雑という方が相応しいと思います。 したがって、エンジニアの主な評価基準はどれだけ難しい問題を解決できるかというより、如何に複雑さを抑えて、大きい問題を簡単に解決する・保つかの方です。 高校時代に歴史と地理学の先生から聞いた言葉ですが、「難しくするのは簡単。簡単にするのは難しい。」というのが印象的で今にも覚えています。 パラダイム シフトとも言えたかもしれません。 「やっぱりわからないから、すごい」のではなく、「思ってたより、ぜんぜん簡単でわかりやすい」の方を目指すべきです。 解決策が簡単だったのはもともとの問題が簡単だったからというのはまずなく、エンジニアが頑張ったから、最終的には簡単な解決になったという方が正しい解釈だと思います。 その中で抽象化はものを簡単に保つための手段となります。 抽象化とは 抽象化という概念自体は抽象的なので、一つの文章で具体的に説明しきるのは難しいですが、以下のように解釈しています。 抽象化とは、特定の問題を概念として分析と分割し、単一の要素として扱えるようにした上で、その要素を組み合わせることでより大きい問題の解決に汎用的に使えるようにすることです。 抽象化をトピックごとにより細かく説明します。 抽象化の目的 大きい問題はいつもより細かい問題で構成されてます。 最上層にある、ユーザーに提供したい高レベルな結果(ボタンで操作できるカート画面)と、最下層にある、実装上の低レベルな詳細(カートの SQL テーブルに適用ポイントを保存する)は両方意識しやすくて、それだけをベースにそのまま開発に入ることがあったかもしれません。 ただし、その両端の間にはとんでもない距離があり、最下層からそのまま最上層を実装しようとすると、結果が凝ったものであればあるほど、開発の効率が下がり、目標は達成しにくくなります。 なぜなら、最下層から最上層まで一気に何かを実装しようとすると、さまざまな、関係のない詳細を一気にかつ同時に気にしないといけなくなるからです。開発の負担が単純に大きすぎるものになってしまいます。 なので、その残念な、非効率な開発環境を避けるために、大きい問題をより小さい問題に分割し、問題を部分的に解決できるようにします。 解決の負担が減った小さい問題を解けられたら、結果の要素を繋ぐことで大きい問題は簡単に解決可能となります。 馴染みのある例に例えるなら、車を作るのが目的な場合、構成が曖昧なまま車を一気につくるより、動力機関、燃料容器、収納スペースなどと概念として分析してから、それぞれの要素を設計・作成し、最終的に全部を組み合わせる方が効率的です。 以上で問題をものに例えたのですが、ことにも例えられます。 生き物としてエネルギーを得るということを分析するのでしたら、食べ物の探し、入手、調理、飲食、分解、摂取などと、ステップにも分けられます。 それが抽象化です。大きい問題をそれぞれより小さい問題に分けて、構造を見出すことです。 どうしても解決できない問題が相手の場合、その問題をそれぞれの概念として分析し、より細かい問題として分割した上で、再度挑戦するのがいいかと思います。 そうすれば、実に難しい問題は意外と少ないかもしれません。 抽象化の特徴 役に立ついい抽象化にはいくつかの特徴があります。大きく分けて、以下の2つにまとめてみました。 問題の構造化 問題を分割するとしても、正しいやり方と正しくないやり方があります。 単 純化 が目的なので、そもそも簡単というのはどういうものなのかを明確にする必要があります。 簡単なものは本質までさかのぼった場合、一つだけなものとして考えるものです。一つの概念、一つのパターン、一つの責任など。複数な概念を合わせることで初めて成立するものであれば、もはや簡単ではないです。 ただし、その一つなものがより小さい複数なもので構成されていても、簡単じゃなくなるわけではないです。一つとして考えられれば、その時点で簡単です。 なので、問題を分割するに当たって、同じように、分割されたものをそれぞれ独立した、一つ一つとして考えられるものにするべきです。 複数な概念がオーバーラップするような、曖昧なものが分割の結果でしたら、それほど問題の単 純化 には貢献しないものとなるからです。 大きい問題を分割する際は、 木構造 の要領でものを分けて、それぞれの部分の大きさを抑えながら、大きいものからどんどん小さいものに構造化するのが望ましいです。 そうすることで、一つ一つの問題の解決は同程度の難易度になって、全体の単 純化 に繋がります。高レベルな問題は低レベルな問題と同じぐらいの努力で解決可能となります。 同時に、特定の細かい問題がどの問題の一部となってるのかも明白になって、把握がしやすくなります。 木の一つのノードを分けるとして、枝の間に共通点が少ない場合は、枝の数も抑えるべきです。 なぜなら、実装で5つの枝を一つのノードに集約するのがそれほど難しくなくても、共通点のない枝が 20本もあれば、集約がそれなりに難しくなります。 どうしても枝の数が多い場合は、共通点となる概念をベースに、一部の枝を一つにまとめて、新しい子ノードで問題をまた分割すれば大丈夫です。 最初は少なくても、改修で一つのノードの枝の数が少しずつ増えないようにするには、最初から分割の結果を、元の問題の 100% をカバーする、同じ抽象レベルのものにするのがいいと思います。 商品の購入過程はかならず選択、購入、受け取りの3つのステップに分けられますので、はじめからそうと分割すれば、後から枝の数が増える可能性が低いです。 まとめると、問題の構造化において、いい抽象化なら、問題は - 独立した概念として分割される - 木構造 として構造化される - 同程度の大きさとして分けられる - 木として各ノードの枝の数が抑えられる - 木として同じノードの枝は同じ抽象レベルにある そんな風に問題を分割すれば、それぞれの問題の解決は実装しやすくなります。 インターフェイス の単 純化 問題がうまく分割されれば、その時点で簡単になります。 ただし、それだけではそれぞれの問題の解決策のつなぎ方が簡単になるとは限らないので、 インターフェイス の面でも複雑さを抑える必要があります。 この項目ではより具体的な説明になるので、問題の解決として「機能」という単語を使います。 特に考慮せず、機能一つ一つをそのまま実装するだけだと、その機能を使うための インターフェイス は機能よりになってしまいます。 ただし、そのそれぞれの機能は皆違いますので、 インターフェイス の間の互換性がいいものにならず、機能を繋ぐだけでかなりな努力が必要となります。 なので、それを避けるため、 インターフェイス 自体を簡単なものに保ち、共通言語でそれぞれの機能をつなげるようにする必要があります。 いい インターフェイス には以下の特徴があります。 インターフェイス は包まれてる機能と同じ抽象レベルで表現されてます 名称(クラス名、メソッド名、引数名など)がその抽象レベルに合わされてます その抽象レベルに合わない実装詳細は表に出ません インターフェイス のエンドポイントは最小限に抑えられてます 機能を活用するために必要なオペレーションのみが公開されてて、利用方法が明白です インターフェイス が必要とする引数の数が抑えられて、少ない加工でもその引数を簡単に提供できます 必要のないデータまでを求めませんが、呼び出し元で準備が必要となってしまう細かすぎるデータも求めません 機能と同じ抽象レベルのデータを引数にします たとえば、商品の価格を計算する機能では、商品モデルを受け取るだけでも問題ありません インターフェイス の返り値も引数と同じルールに従って、他の機能でそのまま活用できます インターフェイス は基本的にステートレスです メソッドをどんな形でどれだけ呼んでも、内部ステートが変わらず、機能の結果に影響しません 最終的のステートを格納するモデルクラスは例外です インターフェイス の実装詳細が隠蔽されてます 呼び出し元が実装の詳細を気にする必要がありません 後から実装が変わっても、変更なく機能をそのまま利用できます(実装の詳細が漏洩しません) インターフェイス はコンテキストには必要以上に依存せず、他のコンテキストでも再利用できます 活用する場合に、必ず他の機能と併用しないと使えない状況に陥ることがありません コンテキストがなくても機能をそのまま理解できます その特徴を持つ インターフェイス を実装するのが難しい時がありますが、どれだけ インターフェイス を高レベルなものに保てたかによって複雑さが決まることが多いです。 インターフェイス 設計の過程で機能の実装自体が難しくなることがありますが、難しい実装と比べて難しい インターフェイス の方は影響が大きいので、選ぶ必要がある時は実装より インターフェイス の方を簡単に保つべきです。 抽象化のメリット うまく抽象化できれば、様々なメリットが現れます。 単純性 全体的にわかりやすくなるので、調査にかかる時間が短縮されます もともと実装した人にとっても、触ったことがない新人にとっても 抽象化を考慮する時間が必要となりますが、実装自体にかかる時間は減ります 問題をそれぞれ個別として扱えるようになるので、一つの問題のみに集中できるようになります 柔軟性 各機能は明確に隔離されるので、一つの機能の修正が他の機能に与える影響が減ります ものをより自由に変更できるようになります 各機能は高レベルな インターフェイス で包まれるので、機能の間に新しい機能を追加するのが簡単になります ドメイン 層の新しい ビジネスロジック など アプリケーション層のキャッシングレイヤーなど 各機能のコンテキストへの依存も抑えられるので、 リファクタリング がよりやりやすくなります 機能の再編など 機能の実行順番の変更など 保守性 単純性と柔軟性の改善から、保守性もそのまま向上されます バグ発生時にどこを修正すればいいのかがより早くわかります 該当箇所を修正したら、漏れが発生しにくくなります 一つの問題が一つのところで対応されるので 密結合状態が避けられるので、 リファクタリング の必要性も減ります 安定性 同じ理由で安定性も改善されます 問題の分割で漏れにはより早く気づくので、仕様漏れやバグの発生率は減ります テスト性 機能一つ一つは独立するので、 ユニットテスト も実装しやすくなります コンテキストとテストデータの準備で必要となる努力は減ります 単一責任に重点が置かれるので、複数の関係のないものを同時にテストする頻度も減ります 抽象化をするには うまく抽象化をするには何を気にするべきか、どのステップを取るべきかを紹介してみます。 概念の分析 抽象化と関係がないことですが、まずするべきなのは対象の案件を具体的なものにすることだと思います。 道標となるメインな仕様があるとして、エッジケースがあるのか、コンテキストが何なのか、未定なところがあるのか、というところを洗い出します。 それができたら、その案件を概念として分析します。 問題を解決するために必要となるデータ(モデルなど)には何があるのかを、「問題の構造化」で紹介した問題の分け方を活かして、分析します。 データを細かく分析できたら、その次に処理(関数など)の分析をします。 問題解決のためにどの処理が必要なのかを洗い出します。 ドメイン 層( ビジネスロジック )とアプリケーション層( フレームワーク )を明確に分けることも望ましいです。 うまく分けて、問題をそれぞれの層の独立したものとして分析できれば、全体が単 純化 されることが多いです。 簡単に実装できそうな大きさの、曖昧なところのない、一つ一つな要素になるまで、データと処理の分析を繰り返します。 抽象化の一番難しい作業は以上の分析になるので、クリアできたら、残りのステップは簡単です。 Tips 分析結果で不可分と見える一つのデータか処理がやはり大きいという印象を抱くことがあるかもしれません。 一見では不可分ですが、大きいと見えたなら、おそらく複数な違う要素でさらに構成されてます。 その要素を暴き出すために、質問を問いて、そのデータか処理の本質を探し出すのがいいかと思います。 そのものは何なのか、何が目的か、実装するには何が必要かなど。 同じく、分析でうまく表現できないデータか処理が現れるかもしれません。 そのものをどう実装できるのかがよくわからない時は新しい概念の導入を検討します。 商品というのはそのまま概念として成り立ちますが、一部の商品のみを扱える処理があるとわかったなら、商品には種類という概念を導入する必要があるかもしれません。 扱い方が全然変わってしまうなら、商品モデルにステートを表すメソッドかカラムを追加するだけのではなく、ラッパークラスを通して、モデルを抽象化するのが妥当な可能性があります(たとえば、購入できない商品対購入できる商品など)。 当然、処理のほうにも新しい概念の導入が必要となる場合があります。 概念としては、ステート、ポリシー、イベント、エラー、アクション、プレゼンター、ストラテ ジー 、エクスト ラク ター、ノーマライザー、 セレクタ ー、ヒストリーなど、ものとことのどちらにも無限とあります。 プログラムに自由に新しい概念を導入しましょう。 インターフェイス の用意 その次に、分析されたものに一つ一つ インターフェイス を与えます。 「 インターフェイス の単 純化 」で紹介した特徴を意識して、簡単な インターフェイス の設計を目標とします。 簡潔にまとめると、以下の特徴を目指します。 用途が伝わる抽象的な名称 数の抑えられたエンドポイント パブリックな関数やメソッドなど 単純な引数と返り値 ステートレスな インターフェイス 実装の詳細を隠蔽した カプセル化 抑えられたコンテキストへの依存 最初から完璧な インターフェイス を設計することが難しい時があります。 そういう時はまず用途を果たすものを作ってから、その インターフェイス を少しずつ改善していく方が効率的です。 基本的に インターフェイス の設計が終わってから、実装に入るべきです。 そうすれば、実装に左右されず、簡単なものが作りやすくなります。 ただし、実装で曖昧なところが多い時は実装をある程度進めてから、 インターフェイス を設計するのもありです。 Tips 名称としては、要素の実装を必要以上に具体的に表さないながら、用途や目的を明確にした、周りと同じ抽象レベルなものが望ましいです。 クラス名、メソッド名、変数名など、どのものにも以上のルールを適用します。 高レベルなコンテキストで、 WriteProductIdToRedis と AddProductToCart の間で後者の方が望ましいでしょう。 なぜかというと、 WriteToRedis と ProductId は実装を直接表すものでありながら、その用途を表してないです。 クラスの実装と呼び出し元を調べないと、用途が何なのかがわからないという問題もあれば、実装が変わった場合、クラス名がその実装と合わなくなります。 簡単な インターフェイス を作るには、実装の詳細とコンテキストを一旦全部忘れて、設計したいデータや処理を ブラックボックス として考えるのがいいと思います。 その インターフェイス でしたら、触ったことのない、コンテキストに疎い新人にとって、そのまま意味をなすものなのかを確認します。 本当に簡単なものであれば、 インターフェイス を見るだけで、大体なことは理解できるはずです。 QA 実装が一つ完成したら、結果を振り返って、抽象化としての質を確かめるのがいいでしょう。 インターフェイス も実装もわかりやすいか 用途と使い方に関してどこかに違和感がないか 単一責任が保たれてるか 実装が顕になってないか その抽象レベルで不可分であるか コンテキストへの依存が少ないか 何かよくないところを発見したら、概念の分析を確認するか、 インターフェイス を調整します。 適用例 初期状況 現在進めている React プロジェクトでは、アナウンスという、特定の条件下で画面に表示される注意事項というものがあります。 同時に、エラーという、サーバーから受け取る動的に変わる説明事項もあります。 画面のデザイン上では、色を除いて、アナウンスとエラーは大体一緒です。 エラーの仕組みはすでに実装されていて、React コンポーネント 内でエラー配列から該当エラーをタグでフィルターして、そのままレンダーするようになっていました。 const renderErrors = () => errors.filter(Error.match( { tag: 'totals' } )) .map((error, index) => <Error key= { index } error= { error } />) return ( <div> { renderErrors() } ... </div> ) 一方で、アナウンスは コンポーネント 内で直接表示すべきかを計算して、そのままレンダーするようになっていました。 const renderCashOnDeliveryMethodAnnounce = () => { if (!(hasDeliveryMethodWithPrepaidFees && isPayOnReceipt)) { return } return <Announce title= "着払いを選択しました。" details= "..." /> } ただし、そのやり方だと、 コンポーネント 一つ一つに表示条件とメッセージの定義を行わないといけなくて、DRY ではないところから保守性が下がります。 エラーの仕組みと似てるところも複数あったので、共 通化 ができるのではないかと思いました。 改善策 メッセージという新しい概念を導入 まず気づいたのは <Error /> と <Announce /> という コンポーネント が大体一緒だったということです。 もとを辿れば、エラーとアナウンスはユーザーに何かを伝えるためのものなので、抽象化して <Message /> としてエラーとアナウンスを再定義しました。 <Message /> はただのメッセージであって、エラーやアナウンスの用途を考慮しないものなので、 インターフェイス は汎用的です。 <Message importance= {} title= {} details= {} /> importance はメッセージの重要度を表しています。値としては info や danger があります。 その importance を使って、メッセージの色が決まりますので、 <Message /> が特に考慮していなくても、呼び出し元でアナウンスとエラーの両方をそのまま表せます。 概念にオーバーラップがないので、 疎結合 となります。 アナウンスという概念を明確に このままでは、アナウンスという概念はコードには明確に現れず、表示条件と組み合わされたメッセージ以上のものにはならないです。 それだと、保守性は上がらず、すべてのアナウンスの改修が必要となれば、箇所の一つ一つを修正しないといけなくなるのと、アナウンスに関するルールも明確になりません。 アナウンスはどこからどこまでのものなのかが曖昧になってしまいます。 なので、その状況でアナウンスを明確なものにするため、アナウンスの定義、略してアナウンスの概念を導入しました。 アナウンスには重要度、文章、表示箇所と表示条件がありますので、定義でそれを明示的に表現します。 // announces.js const ANNNOUNCES = { cash_on_delivery_method_selected: { importance: 'warning' , title: '着払いを選択しました。' , details: '商品価格に含まれていた送料分が引かれますが、別途、着払い料金が必要です。' , tags: [ 'totals' ] , when: ( { product: { hasDeliveryMethodWithPrepaidFees } , deliveryMethod: { isPayOnReceipt } } ) => hasDeliveryMethodWithPrepaidFees && isPayOnReceipt } , // ... } ANNOUNCES オブジェクトのバリューはアナウンスの定義となります。 重要度は importance 文章は title と details 表示箇所は tags で対象オブジェクトを間接的に指定します 表示条件は when でカート商品というモデルを引数に定義します アナウンスはすべて一つのファイル内で定義されてるので、アナウンス横断の修正は簡単になります。 タグでアナウンスが対象にするエリアを定義していますが、どのタグがどのエリアに当たるのかを決めるのは コンポーネント なので、 疎結合 です。 表示条件も、特定の コンポーネント でのみアクセスできるデータを使わず、どのアナウンスにも渡される汎用モデルを引数としているので、コンテキストには依存しません。 あとからアナウンスのタグや表示条件を変えても、コンポネントの方で何も変更なく、アナウンスがそのまま更新されます。 この修正でアナウンス機能は他のものから独立して、一つのものとして扱えるようになりました。 アナウンスとエラーの レンダリング を抽象化 アナウンスとエラーは両方メッセージとなりました。 または、アナウンスの表示箇所はエラーと同じくタグを使って指定できるようになりました。 なので、レンダー処理はアナウンスとエラーの間で抽象化可能となります。 return ( <div> { renderMessages(cartItem, { tag: 'totals' } ) } ... </div> ) 以上のコードでは結局エラーかアナウンスかを意識せず、ただカート商品の、特定のタグのメッセージをレンダーするように単 純化 されました。 エラーは cartItem 内にある errors 配列が使われて、メッセージがレンダーされます。 アナウンスは announces.js 内の定義を対象に、 cartItem と tag を使うことでマッチするものを抽出して、メッセージがレンダーされます。 ただし、呼び出し元ではその内部処理を意識せず、より高い抽象レベルでメッセージをレンダーしてるだけです。 あとから、また違う種類のメッセージを自由に追加できます。 低レベルの詳細が抽象化されて、プログラムの単純性と柔軟性が改善されました。 終わりに 誰もが、ある程度の抽象化は意識せずにできてしまいます。ただし最大までにその概念を活かすには努力と経験が必要となります。 抽象化の目的でライブラリーを活かすのも重要であれば、 ビジネスロジック の抽象化も必要不可欠です。 抽象化をうまく活かせたプロジェクトはリリース後でも修正が容易で、時間が経っても追加開発で特に難しくならないです。 ただし、抽象化されたものが少しずつ具体化して、どんどん変更しにくくならないように、気をつけて常に努力する必要があります。 バランスにも気をつけないといけないです。最大まで抽象化したものが逆に理解しにくくなることもあります。 抽象化と具体化の間のいい中間点を見つけるのが目的となります。ただし、高い抽象レベルでも名称がしっかりしていれば、大体問題にならないと思います。 抽象化は科学的な手順に沿って行うのも可能でしょうが、感に頼って抽象化するのが基本だと思います。 その感を育てるには経験を重ねないといけないですが、メリットが実に大きいので、コストパーフォーマンスがいいです。 新人と経験者の違いの一つは、どれだけ抽象化をうまくできるかというところにあると思います。 最後に、一見では難しいと見えた問題は、抽象化をうまく活かせれば、意外と簡単になります。 概ね、対応中の実装で一つや2つの概念が見えていないからこそ、複雑と感じてしまいます。 プログラム内でその概念を明確に表せれば、複雑さは大体解消されます。 明日の記事の担当はディレクターの神吉さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
Rails アプリケーションに gRPC を導入したときの話 こんにちは、エンジニアの齊藤です。 この記事は Enigmo Advent Calendar 2020 の10日目の記事です。 本日は、 Kubernetes にデプロイした複数の Rails アプリケーション間のデータのやり取りに gRPC を採用した開発について ruby の実装を中心にいくつか共有したいと思います。 ruby を使った gRPC の開発という内容は オフィシャルのスタートガイド で細かく説明されているものがありますので、そちらを踏まえて実際どのように Rails アプリケーションに組み込んだのかという内容でまとめてみました。 まずは Protocol Buffers についてです。 Protocol Buffers によるサービスの定義 gRPC を利用するためには Protocol Buffers という IDL を使ってサービスの内容を定義します。 以下は Seller という出品者情報のエンティティとなるメッセージを定義した上でそれらを操作するためのプロシージャ名とそれぞれのリク エス トパラメーターとレスポンスをメッセージとして定義 したサービスの実装( sellers.proto )の一部です。 サービスの定義でのポイントはエンティティとなるデータ構造は必ず定義して、これを直接レスポンスのメッセージとしては利用せずにリク エス トパラメーターとなるメッセージとレスポンスとなるメッセージを必ずそれぞれ定義するようにしました。 以下の実装でいうと UpsertSellerResponse では単一の Seller をレスポンスで返却し、 GetSellers では複数の Seller を返すような実装になります。 例えば同じ内容のリク エス トパラメーターやレスポンスを複数のプロシージャで共有することは可能だとは思いますが、冗長でもプロシージャごとにそれぞれリク エス トとレスポンスメッセージを用意しておくほうが API としてわかりやすく定義できるのではと思います。 今のプロジェクトの Protocol Buffers を振り返ると一つのサービスにたくさんのプロシージャを定義してしまっているケースも見受けられ、そういった場合は package の記述で ruby のネームスペースを利用することができるのである程度階層化することで解消できたのではと思いっています。 syntax = "proto3"; package Buyma.RPC.PersonalShopper; import "google/protobuf/timestamp.proto"; service Sellers { rpc UpsertSeller(UpsertSellerRequest) returns (UpsertSellerResponse) {} // 出品者情報作成・更新 rpc GetSellers(GetSellersRequest) returns (GetSellersResponse) {} // ページング可能な出品者一覧を取得 } message Seller { uint64 seller_id = 1; string nickname = 2; google.protobuf.Timestamp created_at = 4; google.protobuf.Timestamp updated_at = 5; } message UpsertSellerRequest { unit64 seller_id = 1 string nickname = 2; } message UpsertSellerResponse { Seller seller = 1; // 新規に登録または更新された Seller を返す } message GetSellersRequest { int32 page = 1; } message GetSellersResponse { repeated Seller sellers = 1; // 複数の Seller を返す int32 current_page = 2; int32 total_pages = 3 int32 per_page = 4 } サービス定義からのコード生成 gRPC は Protocol Buffers でサービス内容を定義したファイルからサーバー/クライアント向けのコードを自動生成するツールが提供されています。 それらを利用することでサーバーとクライアントを任意の言語で実装できるようになっています。 今回のケースでは定義( .proto )ファイルと自動生成された ruby のコードを Gem としてパッケージしそれぞれの Rails アプリケーションで利用することにしました。 ちなみに .proto 自体は開発言語に依存しないので複数の言語で利用されることを想定した場合は別の独立した リポジトリ で管理するのが良いと思います。 今回は利用するアプリケーションのスコープと開発言語が ruby に限定されていたので Gem に内包する形で問題ないと判断しました。 ruby で利用するために必要なライブラリは同じく Gem として提供されている grpc と grpc-tools でこれらを gem スペックに記述して導入します。 gruf というライブラリについては後述します。 # 一部省略 Gem :: Specification .new do | spec | # 一部省略 spec.add_dependency ' activerecord ' , ' 6.0.0 ' spec.add_dependency ' activesupport ' , ' 6.0.0 ' spec.add_dependency ' google-protobuf ' , ' 3.9.0 ' spec.add_dependency ' grpc ' , ' 1.22.0 ' spec.add_dependency ' gruf ' , ' 2.7.0 ' # 一部省略 spec.add_development_dependency ' bundler ' , ' ~> 2.0 ' spec.add_development_dependency ' grpc-tools ' , ' 1.22.0 ' end Gem の開発環境は ruby を導入した docker コンテナを利用しました。 以下の grpc_tools_ruby_protoc というコマンドを実行することで必要なコードを生成できます。 Stub と呼ばれるこれらのコードの生成については こちら に詳しく説明がされています。 $ docker-compose exec -w $PWD/lib ash bundle exec grpc_tools_ruby_protoc --ruby_out . --grpc_out . $(cd lib && find . -name '*.proto') このコマンドを実行することによって lib/sellers_pb.rb (定義したすべてのメッセージを含む Protocol Buffers の実装), lib/sellers_service.rb (サービスを実装するための基盤となるクラスとサービスに接続するための Stub と呼ばれるクラス) という2つのファイルが生成されます。 これらを require することでサーバーとクライアントを実装することが可能になります。 今回は更に Gruf という Gem を使ってこれらのコードを Rails アプリケーションに導入しました。 Gruf を使ったサーバー実装 こちら に説明があるように生成されたコードを使ってサーバーの実装を行い gRPC サーバーのプロセスを起動することは可能ですが、既存の Rails アプリケーションにうまく組み込んでいくにはある程度の設計と共 通化 するための機能や設定周りの実装が発生します。 今回はその問題を解消してくれる Gruf というライブラリを使って利用できる実行環境を用意しました。 Gruf については こちらの Wiki に詳しい説明があります。 Gruf を使った RPC サーバーの実装は app/rpc/sellers_controller.rb といった RPC 向けのコントローラーファイルを使い Rails に馴染んだ設計でサーバーの実装をすることが可能になります。 以下のコントローラークラスの例は先程の Sellers サービスのコントローラーです。生成された Buyma::RPC::PersonalShopper::Sellers::Service を bind することでコントローラーのメソッドと マッピング されます。 upsert_seller メソッドは先程の定義ファイルの rpc UpsertSeller の処理を実装したものです。 Buyma::RPC::PersonalShopper::UpsertSellerResponse を返しています。 各コントローラーメソッドには自動的に request というオブジェクトが参照できるようになっており、 request.message でリク エス トの RPC メッセージが参照できるようになっています。 これは Rails コントローラーでリク エス トパラメーターを参照できる params の仕組みに類似した設計だと思いました。 class SellersController < :: Gruf :: Controllers :: Base bind Buyma :: RPC :: PersonalShopper :: Sellers :: Service def upsert_seller Buyma :: RPC :: PersonalShopper :: UpsertSellerResponse .new( seller : Buyma :: RPC :: PersonalShopper :: Seller .new(seller.to_grpc_hash.slice( :seller_id , :nickname , :created_at , :updated_at )) ) end def get_sellers Buyam :: RPC :: PersonalShopper :: GetSellersResponse .new( sellers : sellers, ... end private def seller params = request.message.to_rails_hash end # 一部省略 end seller: Buyma::RPC::PersonalShopper::Seller.new( 部分の実装について Seller メッセージのパラメーターは AcitveRecord の インスタンス である seller から生成していますが seller.to_grpc_hash という実装は先程の Gem のなかで ActiveRecord ::Base に追加しているメソッドです。 google.protobuf.Timestamp 型のフィールドには Time 型の値以外を アサイ ンできない制約があるためこのメソッドで to_time への変換を行うことでその問題を回避しています。 [ 1 ] pry(main)> Buyma :: RPC :: PersonalShopper :: Seller .new( created_at : Time .current) Google :: Protobuf :: TypeError : Invalid type ActiveSupport :: TimeWithZone to assign to submessage field ' created_at ' . from (pry): 32:in ` initialize ` [ 2 ] pry(main)> Buyma :: RPC :: PersonalShopper :: Seller .new( created_at : Time .current.to_time) < Buyma :: RPC :: PersonalShopper :: Seller : seller_id : 0 , nickname : "" , created_at : < Google :: Protobuf :: Timestamp : seconds : 1607499175 , nanos : 658255400 >, updated_at : nil > Gruf は Interceptor と呼ばれる gRPC が提供している、メソッドの前後に処理を行うための仕組みも サポート しています。 今回のプロジェクトでは ActiveRecord::RecordInvalid や ActiveRecord::RecordNotFound などの例外を各サーバーメソッドで共通してハンドリングするために利用しています。 require_relative ' ../error/handler ' module Buyma module RPC module PersonalShopper module Interceptor class HandleError < Gruf :: Interceptors :: ServerInterceptor def call yield rescue StandardError => e Buyma :: RPC :: PersonalShopper :: Error :: Handler .call( self , e) end end end end end end インターセプターの登録は config/initializers で行います。 config/initializers/gruf.rb Gruf .configure do | c | c.interceptors.use( Buyma :: RPC :: PersonalShopper :: Interceptor :: HandleError ) end サーバーの起動 は以下のコマンドで、これで bind したすべてのコントローラーのハンドリングが可能になります。 $ bundle exec gruf Gruf を使ったクライアント実装 クライアント側の実装は Gruf::Client に実行したい RPC サービス名(ここでは Buyma::RPC::PersonalShopper::Sellers ) を指定してサービスに接続するスタブの インスタンス を生成します。 .call メソッドでプロシージャ名とパラメーターを指定して実行することができます。 [ 1 ] pry(main)> client = Gruf :: Client .new( service : Buyma :: RPC :: PersonalShopper :: Sellers , options : { hostname : ENV [ ' GRUF_SERVER ' ] }) [ 2 ] pry(main)> client.call( :UpsertSeller , seller_id : 1 , nickname : ' Foo ' ).message.inspect calling personal-shopper-api-gruf-server: 9001:/ Buyma .RPC.PersonalShopper.Sellers/ UpsertSeller " <Buyma::RPC::PersonalShopper::UpsertSellerResponse: seller: <Buyma::RPC::PersonalShopper::Seller: seller_id: 1, nickname: \"Foo\", created_at: <Google::Protobuf::Timestamp: seconds: 1597815761, nanos: 131317000>, updated_at: <Google::Protobuf::Timestamp: seconds: 1607500796, nanos: 652204000>>> " レスポンスメッセージについても Gem で Google::Protobuf::MessageExts に追加した to_rails_hash メソッドで必要な型変換を行った Hash として取得し以降の処理 ( ActiveRecord の インスタンス の生成等)でデータ型による問題を回避する工夫を行いました。 [ 1 ] pry(main)> client.call( :UpsertSeller , seller_id : 490652 , nickname : ' Foo ' ).message.to_rails_hash calling personal-shopper-api-gruf-server: 9001:/ Buyma .RPC.PersonalShopper.Sellers/ UpsertSeller { :seller => { :seller_id => 490652 , :nickname => " Foo " , :created_at => Wed , 19 Aug 2020 14 : 42 : 41 JST +09: 00 , :updated_at => Wed , 09 Dec 2020 16 : 59 : 56 JST +09: 00 } } gRPC サーバーの HealthCheck Kubernetes にサーバーをデプロイする場合 readinessProbe と livenessProbe の設定に必要になるヘルスチェックの実装についても共有します。 ヘルスチェックの実装には こちら 記事を参考に実装しました。 gPRC には ヘルスチェックのためのプロトコル とそれを実行するための grpc-health-probe というツールがあるのでそちらを導入します。 ツールは gRPC サーバーを起動する Rails アプリケーションコンテナの Dockerfile でインストールしました。 RUN GRPC_HEALTH_PROBE_VERSION=v0.3.1 \ && wget -qO/bin/grpc_health_probe \ https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 \ && chmod +x /bin/grpc_health_probe アプリケーションには Grpc::Health::V1::Health::Service を バインドしたプロシージャを実装をします。 実装内容はアプリケーション上のヘルスチェックを行った上で適切なレスポンスメッセージを返すだけです。 こちらも Gruf のコントローラーとして実装しています。 require ' grpc/health/v1/health_services_pb ' class HealthCheckController < :: Gruf :: Controllers :: Base bind Grpc :: Health :: V1 :: Health :: Service def check if alive? # アプリケーション上のヘルスチェックを実行する Grpc :: Health :: V1 :: HealthCheckResponse .new( status : Grpc :: Health :: V1 :: HealthCheckResponse :: ServingStatus :: SERVING ) else Grpc :: Health :: V1 :: HealthCheckResponse .new( status : Grpc :: Health :: V1 :: HealthCheckResponse :: ServingStatus :: NOT_SERVING ) end end end 動作確認をします。 正常な場合 $ docker-compose exec personal-shopper-api-gruf-server grpc_health_probe -addr=: 9001 status: SERVING 問題がある場合 $ docker-compose exec personal-shopper-api-gruf-server grpc_health_probe -addr=:9001 service unhealthy (responded with "NOT_SERVING") あとは Kubernetes の deployment でヘルスチェックコマンドを導入して完了です。 livenessProbe : exec : command : - grpc_health_probe - -addr=:9001 failureThreshold : 3 initialDelaySeconds : 20 periodSeconds : 10 successThreshold : 1 timeoutSeconds : 5 readinessProbe : exec : command : - grpc_health_probe - -addr=:9001 failureThreshold : 3 initialDelaySeconds : 10 periodSeconds : 10 successThreshold : 1 timeoutSeconds : 5 最後に Rails アプリケーションに gRPC を導入した際のポイントをいくつか共有させていただきました。 今回実際に導入してみて gRPC は IF の仕様が明確にできアプリケーション間で相互にデータをやり取りするようなケースに適している点やそれを仕組み化する基盤としての便利さを体感することができました。 今後の開発でさらに利用する機会を広げて行ければと思いました。 明日は マッドサイエンティスト の ステェーヴェン・ル・ボエデック 氏です。 よろしくお願いします。
アバター
こんにちは。データサイエンティストの堀部です。 この記事は Enigmo Advent Calendar 2020 の9日目の記事です。 何か社外のデータを使っていい感じのことができないかなと思っていたところ、 3日目の竹本さんの記事 がおもしろく、 パクリ 二次創作しました。 短期間で実装したので汚いコードで見苦しいかもしれないですがご了承ください。ちなみに、私は競馬は簡単なルールを知っているくらいでズブの素人です。 目次 使用したライブラリ データ取得 前処理 学習 予測・評価 VSオッズ低い順 VS競馬必勝本 感想 参考資料 使用したライブラリ import urllib.parse import urllib.request as req from time import sleep import category_encoders as ce import lightgbm as lgb import matplotlib.pyplot as plt import numpy as np import pandas as pd from bs4 import BeautifulSoup from selenium import webdriver from tqdm.auto import tqdm インストール手順は割愛します。 *1 データ取得 オッズだけでモデルを組むのはつまらないので簡単に取得できる範囲で下記を追加しました。 馬連 枠 馬名 斤量 騎手 厩舎 馬体重とその増減 年齢 性別 データ取得にあたり、下記の関数とクラスを用意しました。 sleep関数で1秒以上の間隔を空けてnetkeibaから スクレイピング しています。 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" ) sleep( 1 ) 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 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 class HorceRacing : def __init__ (self, race_id, driver): self.race_id = race_id self.driver = driver try : self.result = pd.read_html( "https://race.netkeiba.com/race/result.html?race_id=" + self.race_id ) sleep( 1 ) except BaseException : print ( "no result yet" ) self.odds = self._get_odds() self.info = self._get_info() self.dict_columns = { "馬番" : "horse_no" , "枠" : "gate" , "馬名" : "horse_name" , "斤量" : "burden_weight" , "騎手" : "jockey_name" , "厩舎" : "stable" , "馬体重" : "horse_weight" , "馬体重_増減" : "horse_weight_change" , "性別" : "sextype" , "年齢" : "age" , "オッズ" : "odds" , "着順" : "target" , } def _get_odds (self): self.driver.get( "https://race.netkeiba.com/odds/index.html?type=b1&race_id=" + self.race_id + "&rf=shutuba_submenu" ) html = self.driver.page_source.encode( "utf-8" ) tanhukusoup = BeautifulSoup(html, "html.parser" ) tanhuku_df = pd.read_html( str (tanhukusoup.html))[ 0 ].loc[:, [ "馬番" , "オッズ" ]] sleep( 1 ) return tanhuku_df def _get_info (self): info_df = pd.read_html( "https://race.netkeiba.com/race/shutuba.html?race_id=" + self.race_id )[ 0 ] sleep( 1 ) info_df.columns = [col[ 0 ] for col in info_df.columns] info_df = info_df.loc[:, [ "馬番" , "枠" , "馬名" , "性齢" , "斤量" , "騎手" , "厩舎" , "馬体重(増減)" ]] info_df[ "馬体重" ] = ( info_df[ "馬体重(増減)" ].str.split( "(" ).str[ 0 ].replace( "--" , np.nan).astype( float ) ) info_df[ "馬体重_増減" ] = ( info_df[ "馬体重(増減)" ] .str.split( "(" ) .str[ 1 ] .str.replace( ")" , "" ) .replace( "--" , np.nan) .replace( "前計不" , np.nan) .astype( float ) ) info_df[ "性別" ] = info_df[ "性齢" ].str[ 0 ] info_df[ "年齢" ] = info_df[ "性齢" ].str[ 1 :].astype( int ) info_df.drop([ "馬体重(増減)" , "性齢" ], axis= 1 , inplace= True ) return info_df # 同着があり複数パターンある場合は1番初めのパターンだけ取得 def result_sanrentan (self): _result = self.result[ 2 ].set_index( 0 ).loc[ "3連単" , [ 1 , 2 ]] return ( int (_result[ 2 ].replace( "," , "" ).split( "円" )[ 0 ]), list ( map ( int , _result[ 1 ].split( " " ))), ) def result_sanrenpuku (self): _result = self.result[ 2 ].set_index( 0 ).loc[ "3連複" , [ 1 , 2 ]] return ( int (_result[ 2 ].replace( "," , "" ).split( "円" )[ 0 ]), list ( map ( int , _result[ 1 ].split( " " )))[: 3 ], ) def result_tansyo (self): _result = self.result[ 1 ].set_index( 0 ).loc[ "単勝" , [ 1 , 2 ]] return ( int (_result[ 2 ].replace( "," , "" ).split( "円" )[ 0 ]), list ( map ( int , _result[ 1 ].split( " " ))), ) def get_df (self): df = self.info.merge(self.odds, on= "馬番" ).merge( self.result[ 0 ].loc[:, [ "馬番" , "着順" ]], on=[ "馬番" ] ) # カラムが日本語だとモデルの学習ができないので置換 df.columns = df.columns.map(self.dict_columns) df[ "race_id" ] = self.race_id # 着順が数値以外のものを除外・置換 df = df.loc[~df[ "target" ].isin([ "中止" , "除外" , "取消" ]), :] df[ "target" ] = df[ "target" ].replace( "失格" , 20 ).astype( int ) # オッズが数値以外のものを置換 df[ "odds" ] = df[ "odds" ].replace( "---.-" , np.nan).astype( float ) # lighgbmのlambdarankは数値が大きい方がランクが高いという定義なのでtargetを変換 df[ "target" ] = df[ "target" ].max() - df[ "target" ] + 1 return df データの取得期間は下記のように分けました。 訓練データ:2020年9月5日〜2020年10月31日(540レース分) 検証データ:2020年11月1日〜2020年11月23日(252レース分) テストデータ:2020年11月28日〜2020年11月29日(48レース分) *2 モデルの訓練に使えるような データ形式 で取得し、払戻金の計算が後ほどできるように当たった場合の金額を取得しています。 *3 list_date_train = [ "20200905" , "20200906" , "20200912" , "20200913" , "20200919" , "20200920" , "20200921" , "20200926" , "20200927" , "20201003" , "20201004" , "20201010" , "20201011" , "20201017" , "20201018" , "20201024" , "20201025" , "20201031" , ] list_date_val = [ "20201101" , "20201107" , "20201108" , "20201114" , "20201115" , "20201121" , "20201122" , "20201123" , ] list_date_test = [ "20201128" , "20201129" ] list_train_df = [] dict_train_result = dict () for date in tqdm(list_date_train): race_ids = get_raceids(date) for race_id in tqdm(race_ids): hr = HorceRacing(race_id, driver) train_df = hr.get_df() train_df[ "date" ] = date list_train_df.append(train_df) dict_train_result[race_id] = { "sanrentan" : hr.get_result_sanrentan()[ 0 ], "sanrenpuku" : hr.get_result_sanrenpuku()[ 0 ], "tansyo" : hr.get_result_tansyo()[ 0 ], } list_val_df = [] dict_val_result = dict () for date in tqdm(list_date_val): race_ids = get_raceids(date) for race_id in tqdm(race_ids): hr = HorceRacing(race_id, driver) val_df = hr.get_df() val_df[ "date" ] = date list_val_df.append(val_df) dict_val_result[race_id] = { "sanrentan" : hr.result_sanrentan()[ 0 ], "sanrenpuku" : hr.result_sanrenpuku()[ 0 ], "tansyo" : hr.result_tansyo()[ 0 ], } list_test_df = [] dict_test_result = dict () for date in tqdm(list_date_test): race_ids = get_raceids(date) for race_id in tqdm(race_ids): try : hr = HorceRacing(race_id, driver) test_df = hr.get_df() test_df[ "date" ] = date list_test_df.append(test_df) dict_test_result[race_id] = { "sanrentan" : hr.result_sanrentan()[ 0 ], "sanrenpuku" : hr.result_sanrenpuku()[ 0 ], "tansyo" : hr.result_tansyo()[ 0 ], } except BaseException : pass train = pd.concat(list_train_df) train = train.sort_values( "race_id" ) train.reset_index(inplace= True , drop= True ) val = pd.concat(list_val_df) val = val.sort_values( "race_id" ) val.reset_index(inplace= True , drop= True ) test = pd.concat(list_test_df) test = test.sort_values( "race_id" ) test.reset_index(inplace= True , drop= True ) 前処理 文字列はそのままだとモデルに入れられないので数値に置き換えます。 今回モデルはlightgbmを使うので、 OrdinalEncoder を利用しました。 また、重要そうなオッズを中心に特徴量を追加しました。(関数:add_features) lightgbmのランク学習(lambdarank)の場合、回帰・分類予測と違い上から順に○行は同じレースだよというqueryを用意する必要があるので作成しておきます。 categorical_cols = [ "horse_name" , "jockey_name" , "stable" , "sextype" ] ce_oe = ce.OrdinalEncoder( cols=categorical_cols, handle_unknown= "return_nan" , handle_missing= "return_nan" ) train = ce_oe.fit_transform(train) val = ce_oe.transform(val) test = ce_oe.transform(test) def add_features (df): odds_min = df.groupby( "race_id" )[ "odds" ].min() # オッズの最小値との差分  df[ "odds_diff" ] = df[ "odds" ] - df[ "race_id" ].map(odds_min) # オッズの最小値との倍率 df[ "odds_ratio" ] = df[ "odds" ] / df[ "race_id" ].map(odds_min)   # オッズの偏差値 df[ "odds_deviation" ] = (df[ "odds" ] - df[ "odds" ].mean()) / df[ "odds" ].std() # 斤量 + 馬の体重 df[ "all_weight" ] = df[ "burden_weight" ] + df[ "horse_weight" ] add_features(train) add_features(val) add_features(test) # レースIDを後で参照できるように保持 arr_train_race_ids = train[ "race_id" ].unique() arr_val_race_ids = val[ "race_id" ].unique() arr_test_race_ids = test[ "race_id" ].unique() # ランク学習に必要なqueryを作成 train_query = train.groupby( "race_id" )[ "horse_no" ].count().values.tolist() val_query = val.groupby( "race_id" )[ "horse_no" ].count().values.tolist() test_query = test.groupby( "race_id" )[ "horse_no" ].count().values.tolist() # 学習に不要なカラムを削除 drop_cols = [ "race_id" , "date" ] train.drop(drop_cols, axis= 1 , inplace= True ) val.drop(drop_cols, axis= 1 , inplace= True ) test.drop(drop_cols, axis= 1 , inplace= True ) # 目的変数を分離 target = train_df.pop( "target" ) val_target = val_df.pop( "target" ) test_target = test_df.pop( "target" 学習 モデルはlightgbmのscikit-learn API の LGBMRanker を利用しました。評価指標は NDCG ) *4 です。 lgb_params = { "objective" : "lambdarank" , "metric" : "ndcg" , "n_estimators" : 2000 , "boosting_type" : "gbdt" , "num_leaves" : 31 , "learning_rate" : 0.01 , "importance_type" : "gain" , "random_state" : 42 , } lgb_fit_params = { "eval_metric" : "ndcg" , "eval_at" :( 1 , 2 , 3 ), "early_stopping_rounds" : 50 , "verbose" : 10 , "categorical_feature" : categorical_cols, } lgb_model = lgb.LGBMRanker(**lgb_params) lgb_model.fit( train, target, group=train_query, eval_set=[(train,target),(val,val_target)], eval_group=[train_query,val_query], **lgb_fit_params ) 学習結果です。出馬情報だけだと予測が難しそうですね。 training's ndcg@1: 0.550162 training's ndcg@2: 0.608893 training's ndcg@3: 0.649182 valid_1's ndcg@1: 0.444402 valid_1's ndcg@2: 0.513605 valid_1's ndcg@3: 0.550583 特徴量の重要度(feature_importance)を算出してみました。オッズ関連が予測に効いていますね。 fti =pd.Series(lgb_model.feature_importances_,index=train.columns).sort_values() fti.plot(kind= "barh" ) 予測・評価 検証データ(val)とテストデータ(test)、それぞれに対して予測結果から 単勝 ・3連複・ 3連単 での的中率と100円ずつ買った場合いくら儲かるのか?を計算してみます。比較としてオッズの低い順(人気順)に買ってみた場合も出してみます。 def get_result ( target, pred, query, race_ids, dict_result, is_higher_better= True , bet_yen= 100 ): ind = 0 correct_first = 0 refund_first = 0 correct_sanrenpuku = 0 refund_sanrenpuku = 0 correct_sanrentan = 0 refund_sanrentan = 0 for q, race_id in zip (query, race_ids): _true_first = np.argmax(target[ind : ind + q]) if is_higher_better: _pred_first = np.argmax(pred[ind : ind + q]) else : _pred_first = np.argmin(pred[ind : ind + q]) if _true_first == _pred_first: correct_first += 1 refund_first += dict_result[race_id][ "tansyo" ] / 100 * bet_yen else : refund_first -= bet_yen _true_sanren = np.argsort(target[ind : ind + q].values)[::- 1 ][: 3 ] if is_higher_better: _pred_sanren = np.argsort(pred[ind : ind + q])[::- 1 ][: 3 ] else : _pred_sanren = np.argsort(pred[ind : ind + q])[: 3 ] if len ( set (_true_sanren) & set (_pred_sanren)) == 3 : correct_sanrenpuku += 1 refund_sanrenpuku += dict_result[race_id][ "sanrenpuku" ] / 100 * bet_yen else : refund_sanrenpuku -= bet_yen if _true_sanren.tolist() == _pred_sanren.tolist(): correct_sanrentan += 1 refund_sanrentan += dict_result[race_id][ "sanrentan" ] / 100 * bet_yen else : refund_sanrentan -= bet_yen ind += q print ( f "単勝 的中率: {correct_first / len(query) * 100 :.2f}%, 払戻金-賭け金: {refund_first}" ) print ( f "3連複 的中率: {correct_sanrenpuku / len(query) * 100 :.2f}%, 払戻金-賭け金: {refund_sanrenpuku}" ) print ( f "3連単 的中率: {correct_sanrentan / len(query) * 100 :.2f}%, 払戻金-賭け金: {refund_sanrentan}" ) print ( "-------------------------------------------" ) print ( f "合計 回収率: {((refund_first+refund_sanrenpuku+refund_sanrentan) / (3 * bet_yen * len(race_ids))+1) * 100 :.2f}%, 払戻金-賭け金: {refund_first+refund_sanrenpuku+refund_sanrentan}" ) pred_val = lgb_model.predict(val,num_iteration=lgb_model.best_iteration_) pred_test = lgb_model.predict(test,num_iteration=lgb_model.best_iteration_) print ( "■検証データ(val)" ) print ( "lightgbm モデル" ) print ( "-------------------------------------------" ) get_result( val_target, pred_val, val_query, arr_val_race_ids, dict_val_result, ) print ( "" ) print ( "オッズ低い順" ) print ( "-------------------------------------------" ) get_result( val_target, val[ "odds" ], val_query, arr_val_race_ids, dict_val_result, is_higher_better= False , ) print ( "" ) print ( "■テストデータ(test)" ) print ( "lightgbm モデル" ) print ( "-------------------------------------------" ) get_result( test_target, pred_test, test_query, arr_test_race_ids, dict_test_result ) print ( "" ) print ( "オッズ低い順" ) print ( "-------------------------------------------" ) get_result( test_target, test[ "odds" ], test_query, arr_test_race_ids, dict_test_result, is_higher_better= False , ) VSオッズ低い順 検証データ、テストデータ共にオッズ低い順にかけたよりも回収率が高くなりました!検証データでは残念ながらボロ負けですが、テストデータでは回収率100%超えました。特に3連複・ 3連単 の的中率がオッズ低い順より高くなっているのがおもしろいです。 ■検証データ(val) lightgbm モデル ------------------------------------------- 単勝 的中率: 30.95%, 払戻金-賭け金: 4660.0 3連複 的中率: 6.75%, 払戻金-賭け金: -11460.0 3連単 的中率: 1.59%, 払戻金-賭け金: -17060.0 ------------------------------------------- 合計 回収率: 68.44%, 払戻金-賭け金: -23860.0 オッズ低い順 ------------------------------------------- 単勝 的中率: 32.14%, 払戻金-賭け金: 2450.0 3連複 的中率: 5.56%, 払戻金-賭け金: -15660.0 3連単 的中率: 0.40%, 払戻金-賭け金: -22270.0 ------------------------------------------- 合計 回収率: 53.07%, 払戻金-賭け金: -35480.0 ■テストデータ(test) lightgbm モデル ------------------------------------------- 単勝 的中率: 37.50%, 払戻金-賭け金: 1360.0 3連複 的中率: 14.58%, 払戻金-賭け金: 1060.0 3連単 的中率: 4.17%, 払戻金-賭け金: -900.0 ------------------------------------------- 合計 回収率: 110.56%, 払戻金-賭け金: 1520.0 オッズ低い順 ------------------------------------------- 単勝 的中率: 37.50%, 払戻金-賭け金: 1310.0 3連複 的中率: 12.50%, 払戻金-賭け金: -1040.0 3連単 的中率: 2.08%, 払戻金-賭け金: -3360.0 ------------------------------------------- 合計 回収率: 78.54%, 払戻金-賭け金: -3090.0 VS競馬必勝本 竹本さんの記事では検証データと同じ期間にレースを選択して、3連複のみ購入していました。 金額としては14630円負けてしまいました。 競馬必勝本は本当に当たるのかを検証!〜Pythonで実装する馬券自動選択ツール〜 - エニグモ開発者ブログ そこで検証データの3連複のみ比較すると、 -11,460円(lightgbmモデル × 全レース) > -14,630円(競馬必勝本) > -15,660円(オッズ低い順 × 全レース) ということで、まぐれかもしれないですが 機械学習 で競馬必勝本に勝てたと言ってもよいのではないでしょうか? 感想 なかなか簡単には儲かりませんね。 まだまだ改善の余地がありそうなので、気がむいたら趣味で続けてみたいと思います。 特徴量の追加:コースの情報、天気、血統 取得データ期間の延長 どのレースに賭ける/賭けないべきか?の予測 どの種類の馬券を買うべきか?の予測 参考資料 Welcome to LightGBM’s documentation! — LightGBM 3.1.0.99 documentation LightGBMでサクッとランク学習やってみる - 人間だったら考えて 馬券の種類:はじめての方へ JRA レース情報(JRA) | 出馬表やオッズ、プロ予想などの競馬情報は netkeiba.com 最後まで読んでいただきありがとうございました。 明日の記事の担当はエンジニアの齊藤さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co *1 : 私は poetry を利用しました。 *2 : 後から気づいたのですが、正しく比較するにはテストデータを2020年11月1日〜2020年11月23日にすべきでした。 *3 : 3位以内に同着がある場合は、前処理の簡略化のためサイト上に一番初めに掲載されているパターンのみ取得しています。 *4 : NDCGは0〜1の値をとり、高いほど精度がよいことを示します。
アバター
こんにちは。2021年度より株式会社 エニグモ に新卒で入社することになりました、岡本です。 普段は Mac 使いですが、10月にうっかり注文してしまった Lenovo ThinkPad X13 Gen 1 ( AMD )がもうすぐ届きます。2ヶ月待ちました。WSL2の使い心地をチェックしたいと思います。 この記事は Enigmo Advent Calendar 2020 の7日目の記事です。 私が就職活動を行った2020年は 新型コロナウイルス が流行し、全世界が多大な影響を受けました。そうした中で私がどのように就職活動を行ってきたか、そして内定先企業である株式会社 エニグモ に入社した経緯について、その他諸々綴ります。 いわゆる就活体験記です。 5000字超えの問わず語り、ぜひお付き合いください。 目次 自己紹介 エンジニアを目指したきっかけ 就職活動について(どういった軸で行い、エニグモと出会ったか) コロナ禍での就活 選考について 聞かれたことについて答える 内観し、自己を見出す 技術試験 エニグモの採用(説明会・面接・面談)への印象 エニグモに入社した理由 現在インターンとしてどのような業務を行っているか 今後の抱負 エンジニアを目指す就活生へのメッセージ 自己紹介 2020年11月よりアルバイトとしてサービスエンジニアリング本部に所属し、海外通販サイト『 BUYMA 』のWebアプリケーション開発に携わっています。 大学在学中にプログラミングの学習を始め、企業でWeb アプリ開発 の業務を行ったり インターン に参加していました。 エンジニアを目指したきっかけ 幼い頃から自宅のパソコンでインターネットを閲覧したり創作することが好きでした。小学校にもパソコンがあり、タイピング速度は学校で一二を争うほど速かったです。しかし中高生になった段階で スマートフォン を手に入れ、パソコンを使う機会は激減しました。 高校生の頃、部活動をせずに学校が終わったら急いで家に帰って音楽や深夜ラジオを聞いたり映画やコントのDVDを見たりカルチャー雑誌や文学作品を読んで一人の時間を楽しんでいました。地方で文化に飢えていた私、ライブや劇場に通うために大学進学を機に上京し、更にカルチャーに塗れた生活をしたいなとぼんやり考えていました。 が、紆余曲折あって上京を断念することになり、地元関西の大学の経済学部に進むことになりました。その時「困難に直面しても自力で立ち向かっていける武器を身に付けたい」と思いました。そんな中、書店で手に取った本に「これからの時代はプログラミングスキルを身に付けている人が重宝される」と書いてありました。すぐさまプログラミングについて調べ、Progateで学習し始めました。そこでプログラミングは面白いと思い、できるようになりたいと思いました。 プログラミングの学習を始めてから数ヶ月経過し、その中でスタートアップという世界があることを知りました。また、当時(2017年)は仮想通貨・ ブロックチェーン がトレンドで、それらの技術に興味がありました。 SNS を駆使し、仮想通貨に関する事業を行うスタートアップを見つけて インターン することになりました。 僕が任されたのはエンジニアリング業務ではなく仮想通貨メディアのライター業務でした。エンジニアをやりたいと思ったものの、ライターの業務もやりがいがあり楽しかったです。しかし、社内にいたエンジニアたちがホワイトボードを囲んで議論したり黒い画面に向かってコードを書いているのを横目で見て、やはり彼らのように働けるようになりたいと思いました。また、別のスタートアップの社長の方からもエンジニアになるよう後押しをいただいたこともあり、再びプログラミング学習に力を入れました。 ある程度学習が進んだ大学2年の終盤にさしかかる頃、先輩の紹介でWebエンジニアのバイトを始め、サマー インターン や ハッカソン にも参加するようになりました。 就職活動について(どういった軸で行い、 エニグモ と出会ったか) 満を持して3年時に参加した複数の企業のサマー インターン で、全国から集う優秀なエンジニア学生たちを目の当たりにし、自分の無力さを痛感しました。さらに逆求人形式のイベントに参加した際にも企業から辛辣な評価を受け、完全に心が折れ、自分の行先を見失っていました。情報系の大学/大学院を受験することも考えましたが、色んな人に相談し、エンジニアとして就職する決意を固めました。 コロナ禍での就活 年が明け、企業の本選考に応募し始めた矢先、 新型コロナウイルス が流行し始めました。 エニグモ を始め、各社オンラインでの面接が基本となったため、地方に住んでいる身としては移動のコストが掛からず、大変助かりました。 内定を貰い、アルバイト入社した現在もなお、オフィスに出社したことがありません。社員の方とも直接お会いしたことがありません。しかし、ZoomやSlackでコミュニケーションが取れるため、特に不便は感じていません。(が、せめて直接挨拶したい) こういったことはIT企業、なおかつエンジニアという職だから実現するのだな…と、しみじみありがたく思います。 選考について 選考を受ける過程で、大事だなと思ったことや、準備をしていたことがいくつかあるのでご紹介します。 聞かれたことについて答える 面接を受ける中で一番大事だと思ったのが「聞かれたことについて答えること」です。 簡単なことだと思われるかもしれませんが、意外と難しいことだと思います。 聞かれたことについて無目的に答えるのではなく「事実と意見を区別して答えること」が重要だと感じています。事実について答えるように質問をされているのに自分の意見を話してしまったり、余計なことを答えてしまうことがあるので、聞かれたことについて的確に答えることを普段から意識しています。 内観し、自己を見出す 面接の終盤、面接官の方から「最後に質問はありませんか」と問いかけられることが多いと思います。私はよく面接全体のフィードバックを貰っていました。「僕のことどう思いましたか?」という感じで。指摘されたことはすべて紙のノートやNotionにメモしていました。 例えばある時は「淡々と抑揚のない感じで喋っている」と指摘されたので、次回から少し声を張ったり表情を緩めて話すことを意識するようにしました。 Notionには「就活」という ディレクト リを作り、その下に「就活の軸」「自己紹介」「フィードバック」「想定される質問、それに対する回答」「企業の情報、面接の記録」などのファイルを作って、日々 言語化 を繰り返し、自分は心の中で何を思っているのかを言葉に起こす作業をしました。 心の中で思っていることを言葉にすることの有効性については、T. ウィルソン 村田光二 (訳)『自分を知り、自分を変える―適応的無意識の心理学』( 新曜社 、2005年)の第8章が参考になりました。 自分を知り、自分を変える―適応的無意識の心理学 作者: ティモシー・ウィルソン 発売日: 2005/05/20 メディア: 単行本 会社選びの基準としては「社内のコミュニケーションは活発になされているか、技術の面で自分が貢献できそうか」などを焦点に置いていました。今後リモートワークが継続される可能性は高いため、各社コミュニケーションの仕方をどう工夫しているか面接で尋ねていました。 今までに作ったアプリや関わったサービスについて説明できるようにしておくのは重要です。自分がどんな役割を果たしたのか、こだわりポイントやつまづいたポイント、問題をどう解決したかなどを語れる準備をしました。「喉元過ぎれば熱さを忘れる」という言葉があるように、遭遇した問題やバグは解決してしまえば案外忘れてしまうものです(よね?)。いざ聞かれると答えられないということが分かったので事前に思い出していつでも言えるように準備していました。また、普段使っているWeb技術についても説明できるようにしていました。 技術試験 コーディングテストを課す企業があるので、 AtCoder を使ってA~C問題を解けるように練習していました。未だにコーディングテストは苦手です…。 Webエンジニアの場合は、まずアプリケーション作りを頑張って、使っている技術について説明できることを優先すべきだと思います。 エニグモ の採用(説明会・面接・面談)への印象 就活を始めた当初、 エニグモ を受ける予定はありませんでした。ある日の選考で受け答えがうまくいかず、失望の眼差しで求人サイトを眺めていたところ、偶然 エニグモ の新卒採用の募集が目に留まりました。事業が画期的で技術スタック的にも一致すると思い、軽い気持ちでエントリーしました。 エントリー後の人事面談を経て、1次・2次面接では各回エンジニア2名との面接を行い、最終面接で社長を含む役員3名・人事部長1名との面接を行いました。いずれも面接の翌日には結果をお知らせいただいていました。意思決定の素早さに驚きました。 面接期間は人事の方と連絡を取ることが多いですが、迅速かつ丁寧に対応していただき、好印象を抱きました。 そのほかにも、歳の近い複数の社員の方とのカジュアル面談を組んでいただきました。 エニグモ では BUYMA という安定した基盤がある中で、若手でも柔軟に裁量をもった仕事が出来ると聞き、自分の志向に合っていると感じました。 エニグモ に入社した理由 過去にアルバイトで開発していたC2CのECサービスがあるのですが、クローズすることになりました。会員登録数は増えても購買数が伸び悩んでいて、エンジニアも施策に関して意見を求められる場面がありました。自分なりの提言はしてみるものの目に見える成果は出ませんでした。エンジニアのアルバイトとは言えども、少しでも力になりたかったです。 そこで、利益をあげているECサービスにはどんなエンジニアがいて、どんな開発をしているのか、どうやってビジネスサイドと連携を取っているのか、知りたくなりました。 そんな自分にとって『 BUYMA 』の エニグモ は魅力的でした。会社自体はかねてから官報ブログやStrainerを通じて認知していました。 テックブログを読んだりサービスをチェックし、使われている技術が自分のスタックに一致していることや OSS に関わる方がいることから、ここならエンジニアとしての腕を磨けて、自分が抱える課題にも向き合えると思い入社することにしました。ちなみに内定を承諾するまで1ヶ月ほど検討しました。早期承諾を迫られることはなく、いつまでも待つと仰って頂きました。 現在 インターン としてどのような業務を行っているか 現在大学の卒業を控えており、卒業までの間は11月より エニグモ で インターン として勤務しています。 入社して1ヶ月程度なので、 BUYMA の開発フローの理解に励んでいます。 BUYMA は運用年数が長きに渡る大規模サービスであり、開発の中でいろいろ複雑な点があると感じます。簡単なチケットを割り当ててもらい業務に慣れている段階です。 新卒入社の先輩である平井さんにメンターとしてお世話になっています。チーム長の大川さんも含めて3人で朝会を行い業務をスタートし、疑問があればSlackで伝えたりZoom/ Google meetをつないで ペアプロ をします。業務の終わりに日報を書き、作業内容を整理しています。 普段気をつけていることは「抱え込まない、自分で考える」です。分からないことは速やかに相談しますが、検索したり ソースコード を読んで理解できそうことは自力で解決します。 今後の抱負 4月から社員として本格的に開発に携わる予定です。事業に貢献できるエンジニアになりたいと思います。業務に加え、情報技術者試験の受験、 OSS へのコミットも目標に技術力を磨いていきたいです。 エンジニアを目指す就活生へのメッセージ エンジニアとして生きていく以上、常に勉強をすることが大切です。(自戒を込めて) 初心者あるあるですが、こんな Webサービス を作りたい!と意気込んで大きいものを作ろうとして、結局何も出来ずに終わることがあります。 何か作りたいけど何をすれば良いかわからないという場合には CRUD 操作ができるアプリケーション、 CLI ツールや電卓など、地味で小さいものから作ることをお勧めします。進歩が目に見えてモチベーションが持続しやすいです。 Ruby gemやnpmパッケージの自作もお勧めです。 そして何より、プログラミングを楽しみましょう。エンジニアに限らないことですが、仕事をしたり人と付き合う中でつらいと感じることがあると思います。でも、コードを読み書きすることにすら喜びを感じられないのはもっとつらいです。 リーナス・トーバルズ が Linux カーネル を作ったのは世のためでも人のためでもなく「僕にとって楽しかったから」です。楽しくやりましょう。 Just for Fun: The Story of an Accidental Revolutionary 作者: Torvalds, Linus , Diamond, David 発売日: 2002/06/01 メディア: ペーパーバック 僕から以上です。最後までお読み頂きありがとうございました。 明日の記事の担当はエンジニアの大川さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
はじめに こんにちは、今年の6月に エニグモ に入社したサーバーサイドエンジニアの橋本です。 この記事は Enigmo Advent Calendar 2020 の6日目の記事です。 みなさんは テキストエディタ は何を使っていますか? 会社を見渡すと Vim が一番多いような気がしますが、私は VSCode を使ってます。 正直、 エニグモ に入社するまではツールを入れる程度でそこまでカスタマイズしていなかったのですが、入社してからは諸先輩方の開発スピードに圧倒され、これはツールやショートカットキーを駆使して速く開発できるようにならなければ、、、という必要性に駆られ、少しずつカスタマイズを加えてきました。 この記事では初期設定でも使える便利機能やカスタマイズを加えてよかったショートカットキーやツールをピックアップして紹介していきたいと思います。 ショートカットキー まずは使ってよかったショートカットキーについて紹介します。 VSCode ではデフォルトでショートカットキーが予め設定されていますが、下記の手順でショートカットキーを追加できたり、コマンドを変更できたりします。 ①下記画像のようにcmd + p でコマンドパレットを開き、 >keyboard で検索し、 Keyboard Shortcuts を開きます。 ②下記画像のようにショートカットキー一覧が表示されます。画面上部のバーでショートカットキーが検索でき、編集したい項目をクリックすればショートカットキーを変更することができます。またwhenカラムではショートカットキーを実行するタイミングを設定することもできます。 デフォルトの設定でよく使うショートカットキー それではデフォルトの設定でよく使ったショートカットキーを紹介していきます。 (※ショートカットキーは macOS のキー配置に基づいて書いてます) 操作 コマンド フォルダを開く ⌘+O ファイル検索 ⌘+⇧+F ファイルに移動 ⌘+P ファイルを閉じる ⌘+W 一行選択 ⌘ + L 単語ごとに移動 ⌥+ 矢印キー 単語ごとに選択 ⇧+⌥+ 矢印キー 画面移動 ⇧+⌘+ 矢印キー windowを右へ移動(windowが無い場合は分割) ⌃+⌘+→ windowを左へ移動(windowが無い場合は分割) ⌃+⌘+← 指定の行数まで移動 ^+G+ 行数 一番上まで移動 ⌘+↑ 一番下まで移動 ⌘+↓ 一番右に移動 ⌘+→ 一番左に移動 ⌘+← window1にフォーカス ⌘+1 window2にフォーカス ⌘+2 windowの切り替え ^+W カスタマイズしたショートカットキー 次によく使うのに配置が使いにくかったり、元々割り当てられていなかったりしてカスタマイズを加えたショートカットキーの紹介です。 デフォルトの設定で既に入力しやすいショートカットキーはほとんど割り当てられているので探すのが大変ですが、自分で使いやすいようにカスタマイズを加えると一気に使いやすくなりました。 操作 コマンド ターミナルへフォーカス ^+E ターミナルの分割 ^+V エディタへフォーカス ^+E 相対パス のコピー ⌘+⇧+C 絶対パス のコピー ⌘+⇧+A windowを拡張 ^+⌘+ 矢印キー 相対パス / 絶対パス のコピーはデフォルトの設定だと使いにくいので是非変えてみてください。私はターミナルにフォーカスしても使えるようにwhenカラムは空で設定しています。 導入してよかったツール 導入してよかったツールについて紹介します。 Ruby Ruby を始めた人ならみんな入れると思いますが、一応。 Ruby のコードをハイライトしてくれます。 Ruby Solargraph こちらも Ruby を始めた人なら知っているとおもいますが、メソッドを予測して補完してくれます。 gemのinstallが必要です。 $ gem install solargraph Endwise このツールはendを自動補完してくれるツールでendの書き漏れを防いでくれると同時にコードを書くスピードを上げてくれます。 GitLens GitLensは VSCode でGitをより扱いやすくするツールです。色々な機能があるのですが、このツールで一番便利なのは過去のコミットが追いやすくなることだと感じました。 例えばGitで管理しているファイルで特定の行にフォーカスすることでその行の過去のコミットが表示されます。 また VSCode の左側のBarのGitのマークをクリックすることで現在開いているファイルの過去のコミットを表示させることもできます。 上記のことは git blame でも確認することはできますが個人的にはこっちの方が使いやすかなと思いました。 REST Client Postmanのように API にリク エス トできるツールです。 使い方は簡単で拡張子がrestとなるようなファイルを作ります。 そして今回は例として http://localhost:7700/user_authentication/ に json をpostするリク エス トを書いていきます。 POST http://localhost:7700/user_authentication/ Content-Type: application/json { "user": "user_hoge", "pass": "test_hoge" } ツールを導入している場合、下記画像のように2~3行目の間に Send Request ボタンが表示されます。 このボタンを押すことでリク エス トすることができ、下記画像のようにリク エス ト結果が表示されます。 導入してみて面白かったツール VSCode でこんなこともできるんだーと面白かったので紹介です。 Browser Preview このツールを導入するには事前に Chrome を入れる必要がありますが、なんと VSCode 上でブラウザを開くことができます。 ブラウザと行き来しなくてよくなるのでフロントの開発がしやすくなるかもしれませんが、 Chrome にあるような開発者ツールが無いのが残念。。。 その他やってよかったこと フォルダを統合して ワークスペース を作る 通常、 VSCode では1windowにつき1つのフォルダしか開くことができませんが、 ワークスペース を作成することで複数のフォルダを1windowで開くことができます。 今回は/配下に存在するadventとcalendarの2つのフォルダで ワークスペース を作成し、1つのwindowで開けるようにしていきたいと思います。 $ ls advent calendar まず、adventフォルダを開き、 File>Save Workspace As を開き、workspaceの名前をつけて保存します。(ここではworkspaceの名前はADVENT_CALENDARとしました。) 次に File>Add Folder to Workspace を選択し、workspaceにcalendarフォルダを追加していきます。 すると VSCode 左側のバーにCalendarフォルダが追加されたのが確認できます。 こんな感じでworkspaceを作成することができます! おわりに この記事では私が便利だと思ったツールやショートカットをピックアップして紹介してきましたが、もしもっといいツールがあれば是非紹介してください!! 明日の記事の担当はサーバーサイドエンジニアの岡本さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
アバター
こんにちは。サーバーサイドエンジニアの伊藤です。 この記事は 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
アバター