TECH PLAY

株式会社エニグモ

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

241

こんにちは!Webアプリケーションエンジニアの レミー です! この記事は Enigmo Advent Calendar 2024 の 8日目の記事です。 Ruby on Rails 8が新しくリリースされ、Kamalという迅速かつ便利なデプロイツールが統合されました。私はこれまで Ruby on Rails アプリケーションのデプロイに Capistrano を使用していましたが、Kamalを試してみると、その便利さと簡単さに魅了されました。 この記事では、Kamalを使用して Ruby on Rails 8アプリケーションを AWS EC2サーバーにデプロイする手順を詳しく説明します。 Kamalとは? Kamalは 37Signals のチームによって開発された新しいデプロイメントツールです。このツールを使用すると、デプロイメントプロセスを1つのファイルで定義でき、複雑な手順を簡素化できます。ほとんどのアプリケーションで考慮する必要がない複雑な部分を省略することが可能です。Kamalは、DockerコンテナやTraefikなどのソフトウェアを組み合わせて動作します。セットアップ段階から包括的なソリューションを提供し、 Rails アプリケーションを最短時間で本番環境にリリースできます。 Kamalはどのように動作するのか? Kamalは、サーバー上でDockerコンテナ内にWebアプリケーションを実行し、Traefikを組み合わせてネットワーク トラフィック を処理します。新しいバージョンをデプロイする際、Kamalは以下の手順を実行します: 新しいDockerイメージをビルドする。 新しいイメージからコンテナを起動する。 新しいコンテナが正常に動作しているかを検証する。 Traefikを更新して、 トラフィック を新しいコンテナにルーティングする。 古いコンテナを停止する。 特に注目すべき点は、ダウンタイムなしのデプロイメントを実現し、blue/greenデプロイメントをサポートしていることです。 Ruby on Rails アプリケーションのサーバーの準備 ここでは、 AWS EC2でサーバーを準備します。 EC2 インスタンス の作成 Ubuntu Server 24.04 を選択します。 Keyペアは SSH とサーバーへのアクセスに使用します。 セキュリティグループでは、以下のポートを開放してください: 22 : SSH 用 80 : HTTP用 443 : HTTPS 用 SSH 権限の設定 まず、ローカルの公開鍵をコピーします。 cat ~/.ssh/id_rsa.pub インスタンス を起動したら、先ほど作成したKeyペアを使って、 ubuntu ユーザーでサーバーに SSH 接続します。 IPアドレス は新しく作成した インスタンス のものを使用します。 ssh ubuntu@ 18.182 . 197.19 -i /path/to/key.pem 次に、ローカルでコピーした公開鍵をサーバーの ~/. ssh /authorized_keys ファイルに貼り付けます。 ssh-rsa AAAABEAAAADAQABAAABgQ...AAAAB3Nzac2EAAAADAQABA Yuto MacBook Pro これでサーバーの準備は完了です。以降は以下のコマンドでサーバーに SSH 接続できます。 ssh ubuntu@ 18.182 . 197.19 Ruby on Rails 8アプリケーションの作成 まず、kamaltest という名前で Ruby on Rails 8アプリケーションを作成します。 rails new kamaltest プロジェクト ディレクト リに移動します。 cd kamaltest このアプリケーションは、タイトルと内容を持つ簡単なブログになります。そのため、scaffoldを使用して構築します。 rails g scaffold article title content:text データベースを作成します。 rake db:migrate routes.rb ファイルでホームページを設定します。 root "articles#index" アプリケーションを起動します。 bin/dev その後、 http://localhost:3000 にアクセスすると、アプリケーションが起動していることが確認できます。 deploy.ymlファイルでKamalを設定する Ruby on Rails 8ではKamalが既に統合されています。それ以前のプロジェクトにKamalを追加する場合は、Gemfileに kamal を追加し、以下のコマンドを実行します。 kamal init このコマンドを実行すると、Kamalがアプリケーションをデプロイするための設定を含む config/deploy.yml ファイルが生成されます。 Kamalの基本設定 デプロイのための設定は以下のようになります。 image: yutoyasunaga/kamaltest servers: web: - 18.182.197.19 # server IP proxy: ssl: true host: kamaltest.sampleapp.net registry: username: yutoyasunaga password: - KAMAL_REGISTRY_PASSWORD ssh: user: ubuntu image : Docker Hub上に保存されるDockerイメージの場所 servers web : サーバーの IPv4 アドレス proxy ssl : true に設定すると SSL が自動的に設定されます host : 使用する ドメイン registry username : Dockerアカウントのユーザー名 password : Dockerアカウントのログ インパス ワード。セキュリティのため、 KAMAL_REGISTRY_PASSWORD シークレットを通じて取得します。 ssh user : サーバーに SSH 接続する際のユーザー Kamalのシークレット 機密情報は .kamal/secrets ファイルに配置します。以下はその例です。 # 環境変数からレジストリパスワードを取得 KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD シークレットは 環境変数 から取得します。例えば、Dockerのパスワードが hogehoge の場合、以下のように設定します。 export KAMAL_REGISTRY_PASSWORD= 'hogehoge' 以下のコマンドで値を確認できます。 echo $KAMAL_REGISTRY_PASSWORD Kamalでデプロイを開始 config/deploy.yml の設定が完了したら、以下のコマンドを使用して初回のデプロイを開始します。 kamal setup このコマンドは以下の処理を行います: SSH キーを使ってサーバーに接続。 サーバーにDockerがインストールされていない場合は、get.docker.com を使ってDockerをインストール( SSH 経由でrootアクセスが必要)。 レジストリ にローカルおよびリモートでログイン。 アプリケーションのルートにある標準的なDockerfileを使用してDockerイメージをビルド。 イメージを レジストリ にプッシュ。 レジストリ からイメージをサーバーにプル。 kamal-proxy がポート80および443で トラフィック を受け入れることを確認。 現在のGitバージョンのハッシュに一致するアプリケーションバージョンで新しいコンテナを起動。 新しいコンテナが GET /up リク エス トに対して200 OKを返す場合、kamal-proxy に トラフィック を新しいコンテナにルーティングさせる。 前バージョンのアプリケーションを実行している古いコンテナを停止。 使用されていないイメージや停止したコンテナを削除して、サーバーの容量を確保。 デプロイが成功すると、次のような結果が表示されます。 Finished all in 70.0 seconds 初回のデプロイは kamal setup を使用しますが、2回目以降のデプロイでは kamal deploy のコマンドを使用します。 permission denied エラーの解決方法 初回のデプロイで以下のようなDockerに関連するエラーが発生した場合: Releasing the deploy lock... Finished all in 47.9 seconds ERROR (SSHKit::Command::Failed): Exception while executing on host 54.250 . 243.158 : docker exit status: 1 docker stdout: Nothing written docker stderr: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/images/create?fromImage=yutoyasunaga%2Fkamaltest&tag=4985d03bae739286203ce1185efdd4b2c71f90a9" : dial unix /var/run/docker.sock: connect: permission denied 次の手順で解決できます。 サーバーに SSH 接続します。 ssh ubuntu@ 18.182 . 197.19 以下のコマンドを実行して、現在のユーザーをDockerグループに追加します。 sudo usermod -aG docker $USER && newgrp docker Dockerが正常に動作するか確認します。 docker ps 以下のような出力が表示されれば成功です。 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES デプロイ後の結果を確認 まず、 https://hub.docker.com にアクセスして、イメージがプッシュされていることを確認します。 Route53を使用して ドメイン を管理している場合、以下のようにホストゾーン内でレコードを作成し、 ドメイン をサーバーのIPに向けます。 設定が正しい場合、 https://kamaltest.sampleapp.net にアクセスすると、アプリケーションの画面が表示されます。 このように、1分ほどで Ruby on Rails アプリケーションをサーバーにデプロイし、 ドメイン と SSL の設定まで完了しました。便利ですね! 🎉 よく使われるKamalコマンド aliases: console: app exec --interactive --reuse "bin/rails console" shell: app exec --interactive --reuse "bash" logs: app logs -f dbc: app exec --interactive --reuse "bin/rails dbconsole" デフォルトの deploy.yml ファイルでは、以下の4つの便利なコマンドが定義されています。 kamal console : Rails コンソールにアクセス kamal shell : サーバー上のコンテナにアクセス kamal logs : サーバーログを確認 kamal dbc : データベースコンソールにアクセス Kamalでのアセットのデプロイ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile Dockerfile にはすでにアセットをプリ コンパイル するコマンドが定義されています。そのため、デプロイ時にアセットも自動的に処理されます。 Kamalでの 環境変数 (Environment Variable) 開発環境の 環境変数 の設定 開発環境では、 dotenv gem を使用します。 公式 リポジトリ : https://github.com/bkeepers/dotenv Gemfileに dotenv を追加 以下のコードを Gemfile に記載します。 group :development, :test do gem 'dotenv' end 環境変数 を管理する .env ファイルを作成し、例は以下のように記載します。 TEST_ENV_CLEAR=env_clear_local TEST_ENV_SECRET=env_secret_local 環境変数 が正しくロードされているかを Rails コンソールで確認します。 ENV .select { |key, _| key.start_with?( " TEST_ENV " ) } => { " TEST_ENV_CLEAR " => " env_clear_local " , " TEST_ENV_SECRET " => " env_secret_local " } 本番環境の 環境変数 の設定 現時点では以下の方法を使用していますが、より良い方法が見つかれば変更する予定です。 本番環境用の 環境変数 を管理する .env.production ファイルを作成します。 TEST_ENV_SECRET=env_secret_prod deploy.yml ファイルに 環境変数 を設定します。 env: secret: - TEST_ENV_SECRET clear: TEST_ENV_CLEAR: env_clear_prod clear : deploy.yml ファイルに直接記載される 環境変数 。 secret : 機密情報を含む 環境変数 で、 .kamal/secrets ファイルから読み込まれます。 .kamal/secrets ファイルで 環境変数 を以下のように設定します。 TEST_ENV_SECRET=$(cat .env.production | grep TEST_ENV_SECRET | cut -d '=' -f 2) 上記の設定は、 .env.production ファイルから情報を抽出します。 例えば、 .env.production に以下の行が含まれている場合: TEST_ENV_SECRET=env_secret_prod コマンド cut -d '=' -f 2 によって env_secret_prod が抽出されます。 デプロイ後、 Rails コンソールにアクセスして 環境変数 を確認できます。 kamal console ENV .select { |key, _| key.start_with?( " TEST_ENV " ) } => { " TEST_ENV_SECRET " => " env_secret_prod " , " TEST_ENV_CLEAR " => " env_clear_prod " } Kamalは、 Ruby on Rails アプリケーションのデプロイを大幅に簡素化し、効率化する強力なツールです。ぜひKamalを使ったデプロイに挑戦してみてください! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは!UIUXデザイナーの和田です。 こちらは Enigmo Advent Calendar 2024 の7日目の記事です。 この記事では、定期的に実施している BUYMA のユーザーインタビューについてご紹介します。 また、ユーザーインタビューで得た気付きをもとに新設した【 まるわかりBUYMA公式スタートガイド 】についても後述します。 www.buyma.com 1. ユーザーの声を直に聞くメリット エニグモ では、社内のデータアナリストがBUMYAの利用データを分析して日々サービス改善に努めています。 定量 的な分析に加えて、的確なUX改善・向上を進めていくためには、ユーザーさまの声を直にお聞きして、なかなか数値からは解明できない 【ユーザーの行動の背景・ インサイト 】 をより深く理解することが重要だと考えています。 BUYMA では、ユーザーさまの声を直にお聞きするために、 定期的にオンライン形式(Zoom)のユーザーインタビューを実施しています。 社内のUIUXデザイナーとデータアナリストがインタビューアーをすることで、ユーザーさまからいただいたご意見やヒントをもとに「すぐに詳細な分析をおこない、施策化して改善を進められる」ようにしています。 2. BUYMA のユーザーインタビュー BUYMA では、ユーザーさまを対象に実施しているユーザーアンケートの中でインタビューにご協力いただける方を募っています。 毎回とても多くの方に、インタビューへのご協力に前向きな回答をいただいており、全国男女問わず幅広い年代の方にご参加いただいています! ユーザーインタビューの内容についてご紹介します。 🌟 2-1.ユーザーさまご自身について ユーザーさまご自身についてのお話を広くお聞きしています。 主に、『普段の生活スタイル』や『どのような「価値観・嗜好」をお持ちなのか』といった点や、 BUYMA との接点になりそうな「ファッション情報にふれる機会」「お買い物傾向」に関するお話などを伺っています。 🌟 2-2.サービス利用に関する ヒアリ ング ユーザーさまが普段 BUYMA を『どのように利用して』『どのように感じていらっしゃるか』、実際の利用エピソードをもとにプラスとマイナスの両側面から率直な話をお聞きできるようにしています。 ・ BUYMA の利用をはじめたきっかけ ・ BUYMA を使う理由( BUYMA の価値) ←→  BUYMA を使わなくなった理由(離脱理由) ・ BUYMA の気に入っているところ ←→ いまいちだと感じるところ、残念なところ ・ BUYMA を安心して利用できるポイント ←→  BUYMA に不安を感じるポイント ・ BUYMA でのよかった・満足できたエピソード ←→  BUYMA でのマイナスエピソード BUYMA において価値を感じていただいているポイント → より多くの方にその価値を広げていけるように BUYMA における課題・ペイン → 課題の背景を理解して適切な改善につなげていけるように 上記のように、プラス・マイナスそれぞれの気付きをもとに、UX改善・向上にむけた検討を進めています。 🌟 2-3.利用を見せていただく(行動観察) 実際にアプリやサイトを利用している様子を画面共有いただいて、どのように BUYMA をご利用いただいているか見せていただきつつ、お話を伺っています。 例えば、ユーザーさまの普段の利用(検索や商品閲覧・比較検討など)に即したシナリオをベースに、どのような操作をどのような手順でされているのか、利用の中の些細なつまずきやペインがどういったところにあるか、などを操作いただきながら ヒアリ ングを進めています。 🌟 2-4.サービスに関する認知・理解に関する ヒアリ ング BUYMA サービス・機能における「認知(知っていたか)」や「理解(理解できていたか)」についてもお聞きしています。 『どこで認知・理解したか』『どのように解釈していたか』なども率直にお聞きすることで、【対象サービス・機能の露出や導線が十分であるか】など、改善のヒントをいただけるようにしています。 🌟 2-5.検討中の施策に関するコンセプト ヒアリ ング 検討中の施策に関するプロトタイプをZoomミーティングの中で投影させていただきながら、率直な印象やご意見をお伺いしています。 施策を進める前に、初見の印象やニーズにマッチしているかなどを確認することができ、よりよいかたちに細かな調整をかけることができます。 BUYMA のUXリサーチについては、昨年の記事でもご紹介しておりますので、ぜひこちらもご覧ください。 tech.enigmo.co.jp 3. ユーザーの声から「気付くこと」「改めてわかること」がある ユーザーにとっての「ペイン・課題」「 BUYMA の価値」など、ユーザーの声から「気付くことができること」「改めてわかること・理解が深まること」がたくさんあります。 🌟ユーザーにとっての「ペイン・課題」に関する気付き BUYMA 利用におけるつまずきがどんなタイミングで起こっているのか、「 BUYMA 利用におけるペイン・課題」がどのくらいUXに影響を及ぼしているか、など、ユーザーインタビューを通じて具体的に理解を深めることができます。また、サービス提供側として想定していた捉え方と、ユーザーさまの捉え方が異なっていたり、思った以上に浸透していない情報についての気付きを得ることもあります。 🌟ユーザーにとっての「 BUYMA の価値」に関する気付き 一方で、ユーザーさまにとって「 BUYMA の価値」がどのように認識されていて、ユーザーさまのまわり(ご家族やご友人)に浸透しているか、などの気付きを得ることもできます。 以下では、ユーザーインタビューを通じて、改めて BUYMA の価値だと再認識することができた機能についてご紹介します。 世界中からさがす|リクエスト一覧 BUYMA には『リク エス ト』という機能があります。 世界中にいる BUYMA の出品者に、ほしい商品を探してもらうことができる機能ですが、 インタビューでは、この機能を利用して購入経験のある方は「リク エス ト機能の満足度がとても高い」ことがわかりました。 「どこを探しても見つからなかった国内完売商品がほしくて、 リク エス トを利用してみたら見つかって購入できて本当に嬉しかった!」 「ずっと探していたバッグがなかなか見つからず、中古は状態がいいものがなく諦めかけていたけれど、リク エス トを使って購入することができて非常に満足。」 また、インタビューの対象者の中でリク エス ト機能を使ったことがある方は、複数回リク エス トを利用しており、 「探したいと思うアイテムをリク エス トして世界中から探してもらう」体験を気に入ってくださっている傾向がありました。 今後は、「探したいアイテムがあるけれどリク エス ト機能を知らない・使ったことがない方」に、 このプラスの体験をより広げていけるように検討を進めていければと考えています。 4. ユーザーの声からまるわかり BUYMA 公式スタートガイドを作った話 ユーザーインタビューが終わったあとは、まとめ作業を行って主要な気付き( BUYMA の価値、ペイン・課題、仮説検証の結果など)を社内に共有しています。 そこから、UX改善・向上につなげる施策化をおこなっています。 今回は、ユーザーインタビューで得た気付きからコンテンツ作成を行った事例を紹介します。 まるわかりBUYMA公式スタートガイド 4-1.まるわかり BUYMA 公式スタートガイド新設の経緯 インタビューの中では、 BUYMA に関する分からないことや不安なことについてもお伺いしています。 その中で、以下のようなご質問・ご意見をいただくことがありました。 「 BUYMA の運営会社って日本企業なんですか?」 「 BUYMA の商品ってなんで安いんですか?」 「 BUYMA でほしい商品があるけれど購入して大丈夫なのか不安があります。」 このようなご質問・ご意見をいただいたことをきっかけに『 BUYMA を安心してご利用できると判断するための情報の重要性』について改めて認識することができました。 その後、社内で検討を進め「 BUYMA とはどういったサービスなのか」「安心して利用できるのか」などの疑問や不安を解消いただくために、【 まるわかりBUYMA公式スタートガイド 】というページを新設することになりました。ぜひこちらもご覧ください! www.buyma.com インタビューの中で BUYMA に関する分からなさや不安があるとお話されていたユーザーさまから、 いろいろと会話させていただいたインタビューの最後に 「思い切ってインタビューに参加してみてよかった。これまでより安心して買い物ができそう。」 とおっしゃっていただいたことがありました。 ユーザーインタビューを通して、ユーザーさま1人1人に向き合うことの大切さを実感するとともに、 UX改善・向上に向けて一歩ずつ前進するためのエネルギーをいただけていると感じています! 4-2. BUYMA ユーザーの声をよりたくさんの人に広げたい ユーザーインタビューの中で、積極的にご利用いただいているユーザーさまは、「まわりのご家族やご友人も BUYMA をご利用いただいている方が多い」という傾向が見えてきました。 一方で、 BUYMA でのお買い物を躊躇される理由として、「まわりに BUYMA を利用している人がいない」といった方が多くいらっしゃいました。 そこで、『どんな人が BUYMA を利用しているかイメージがつくことで親近感がわくようにするとよいのではないか』という仮説から、 BUYMA をご利用いただいているユーザーさまのお声を BUYMA のコンテンツとして掲載させていただくことにしました。 🌟 BUYMA ユーザーが感じる BUYMA のよさ BUYMA の利用者が「どんなところに BUYMA の魅力を感じてくださっているのか?」より多くの人に知っていただくために、 BUYMA を使う理由についてユーザーさまのご意見をピックアップしています。 BUYMAユーザーに聞く|なぜBUYMAを使ってるの? 🌟 BUYMA ユーザーが安心して利用できる理由 BUYMA を安心して利用してくださっている利用者が「どうして BUYMA を安心して利用できると判断してくださったのか?」より多くの人に知っていただくために、 BUYMA を安心についてお聞きした際のご回答をピックアップしています。 BUYMAユーザーに聞く|安心して利用できてる? www.buyma.com BUYMA についてより深く知っていただき、安心してご利用を続けていただけるように、今後も BUYMA のことをもっと深くわかりやすい情報を発信していければと考えています。 明日の記事の担当は・・・ 【 BUYMA サイトの開発と運用保守をされている レミー さん】です! お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、データサイエンティストの髙橋です。業務では企画/分析/ 機械学習 モデル作成/プロダクション向けの実装/効果検証を一貫して行っています。 この記事では類似画像検索システムの内製化にあたり、システム面での課題をどのように解決したかについて紹介します。内製化の背景や 機械学習 部分などについては 以前作成した記事 で説明しており、この記事はその続きとなります。 この記事は Enigmo Advent Calendar 2024 の 5 日目の記事です。 内製化の目的・事業インパクト 類似画像検索のシステム化における課題 システム化における各課題の解決策 「商品画像ダウンロード > セグメンテーション > Embedding 計算 > アップロード」の高速化 Embedding 同士の類似度計算処理の高速化 画像ハッシュ計算処理の高速化 複数 GCP サービスを連携させてスケジュール実行するアーキテクチャ作成 実装時の工夫 Cloud Composer からの GCP サービス呼び出し方法 Vertex AI Vector Search での類似度計算時にリトライ まとめ 追記:2025/01/21 第48回 MLOps 勉強会で登壇しました! 内製化の目的・事業 インパク ト 内製化の目的は、弊社が運営する CtoC EC サイト BUYMA において、商品 名寄せ で利用している他社製の類似画像検索システムの精度を保ちながらコストを削減することでした。既に内製化後のシステムに移行しており、同等の精度を維持しつつ年間数百万円規模(約8〜9割)のコスト削減を見込んでいます。 また、 名寄せ 以外にも類似画像検索システムを利用している施策があり、その移行も進めることでさらなるコスト削減の可能性があります。さらに、他社製の類似画像検索システムではコストが高く断念していた EC サイト上での画像起点のレコメンドや、画像による類似商品検索機能なども検討出来るようになりました。 より詳細な説明については 前編である機械学習編の記事 を参照ください。 類似画像検索のシステム化における課題 類似画像検索のシステム化における課題として、非常に大規模なデータを現実的な時間で処理する必要がありました。 機械学習編の記事に記載した類似画像検索の各ステップ に対して、毎月処理するデータ量は以下の通りでした。 ステップ 毎月処理するデータ量 商品画像をダウンロードし、物体存在箇所をセグメンテーションして切り抜き 約 200 万画像 切り抜いた画像の Embedding 計算 約 1000 ∼ 2000 万画像 切り抜いた画像・Embedding ファイルを GCS ( Google Cloud Storage) にアップロード 約 2000 ∼ 4000 万ファイル Embedding による画像同士の類似度計算 数十〜数千億の組合せ 画像ハッシュによる画像同士の類似度計算 数十万の組合せ 単純に単一のサーバー上で各ステップを実行する方法では、全体で1ヶ月以上かかる見込みであり、毎月定期的に処理を行うのは現実的ではありませんでした。 また、データ量以外の課題として、弊社では 機械学習 基盤として Vertex AI Pipelines を利用していましたが、今回のシステムはその基盤上に実装できない課題がありました。先述した規模のデータを処理するにはそれに特化した複数の GCP ( Google Cloud Platform) サービスを組み合わせる必要がある一方で、現行の基盤は VM インスタンス 上で Python コードを実行する用途を想定していたためです。そのため、複数の GCP サービスを連携して毎月スケジュール実行する アーキテクチャ を作る必要性がありました。 システム化における各課題の解決策 前述した課題をどのように解決したかを説明します。ただし、前提として類似画像検索の各ステップのうち、以下は1つのかたまりとして処理することにしました。 商品画像をダウンロードし、物体存在箇所をセグメンテーションして切り抜き 切り抜いた画像の Embedding 計算 切り抜いた画像・Embedding ファイルを GCS にアップロード 理由は、セグメンテーションおよび Embedding 計算の両方に GPU が必要であったため、また GCS とのデータのやり取りに時間がかかることから、「商品画像ダウンロード > セグメンテーション > Embedding 計算 > アップロード」の一連のステップを同一メモリ上で行いたかったためです。 「商品画像ダウンロード > セグメンテーション > Embedding 計算 > アップロード」の高速化 Dataflow  という並列分散処理が行える GCP サービスを利用することで高速化を実現しました。Dataflow とは、 GCP が提供するマネージドのバッチ・ストリーミングデータ処理サービスであり、並列分散処理により大量のデータを効率的に処理することが可能です。 今回 Dataflow を選択するにあたり、 Ray on Vertex AI も候補に上がりました。いずれも並列分散処理は実現できそうでした。Ray はその構文がネイティブの Python に近く、既存の Python コードに大きな変更を加える必要がなさそうだったため、実装コストが低そうに見えました。一方で、Dataflow は Apache Beam の構文を理解し覚える必要があり一定の学習コストが伴いそうでしが、社内で利用実績があり困ったときに既存の資産を参考に出来そうであったため Dataflow を選択しました。 実際に、 Dataflow の実装で困ったときに他プロジェクトでの ソースコード を参考にして解決することができ、この選択は正解であったと考えています。また、 Apache Beam の構文もそれほど複雑ではなく、初期の学習コストは多少ありましたが慣れれば実装に大きく手間取ることはなかったです。 約 200 万枚の画像に対して、並列分散処理無しでは約 30 日かかる見込みでしたが、Dataflow により約 56 時間に短縮することができました。 Dataflow の設定は ワーカーマシンタイプ: n1-highmem-4 (vCPU 4 、メモリ 26 GB) ワーカー数: 4 GPU :  nvidia -tesla-t4 としました。ハイメモリのマシンタイプを利用した理由は、メモリ枯渇でジョブが途中で停止してしまうことがあったためです。 Embedding 同士の類似度計算処理の高速化 Vertex AI Vector Search というサービスを利用しました。Vertex AI Vector Search とは、 GCP が提供するマネージドサービスで、膨大な数の Embedding 同士の類似度計算を高速に行うことができます。 Vertex AI Vector Search を利用することで約 1700億個の Embedding の組合せを約 8 時間で処理することができました。単一のサーバー上で処理した場合の処理時間は見積もっていませんが、おそらくこの規模のデータをこの速度で処理するシステムを作るにはそれなりの 工数 がかかったと思います。 Vertex AI Vector Search の設定としては、Algorithm type 、 Shard size はそれぞれデフォルト値である tree-AH 、 Medium で速度やコストに問題がなかったためそのままとしました。 Approximate neighbors count は値を変えて実験したところ検索速度に大きな違いが生じました。具体的には、 約 1 千万件の 768 次元のベクトルに対して類似度計算を 100 回行い、処理速度の統計量を算出したところ以下の通りでした。 num_neighbors mean (sec) std (sec) min (sec) max (sec) 10 0.031 0.013 0.021 0.076 100 0.189 0.034 0.146 0.266 1000 0. 218 0.034 0.17 0.297 10000 0.519 0.149 0.39 1.109 上記実験結果より可能な限り低い値にすることで処理時間が大きく短縮できそうでした。今回の用途では 1 画像に対して同一と検知される見込み画像数は数件程度であったため、それをカバーできる 10 としました。 画像ハッシュ計算処理の高速化 このステップでも Dataflow を利用しました。 約 27 万件の画像の組合せに対して、並列分散処理無しでは約 54 時間かかる見込みでしたが、Dataflow により約 45 分に短縮することができました。 Dataflow の設定は ワーカーマシンタイプ: n1-standard-1 (vCPU 1 、メモリ 3.75 GB) ワーカー数: 120 としました。Dataflow を採用したことで、ワーカー数を自由に変更することができ、120 ワーカーで並列分散処理を容易に実現できました。 複数 GCP サービスを連携させてスケジュール実行する アーキテクチャ 作成 Cloud Composer という Airflow のマネージドサービスを利用しました。Cloud Composer は、 GCP が提供するワークフロー オーケストレーション サービスで、複数の クラウド サービスを連携してスケジュール実行することができます。 今回 Cloud Composer を選択するにあたり Cloud Workflows も候補に上がりましたが、 Airflow の社内での利用実績が豊富であったため Cloud Composer を採用しました。こちらでも困ったときに他プロジェクトの既存の ソースコード が参考になる場面が多く、また移行前の類似画像検索システムの一部で Airflow を利用しており、既存システムの理解がスムーズに出来たメリットもありました。 以下のようなフローで Dataflow や Vertex AI Vector Search などを連携し、類似画像検索の各ステップを実行するシステムを Cloud Composer で実装しました。 開発生産性や保守性を向上させるために、 Dataflow では各ステップごとに Docker Image と Flex Template を、 Cloud Batch でも Docker Image を利用しました。これにより各ステップを独立に開発・テスト・デプロイ出来るようにしました。この部分の詳細については、別途機会があれば記事として執筆する予定です。 実装時の工夫 ここでは、システム実装時の工夫を2つ紹介します。同じようなシステム構成の実装をされる方の参考になれば幸いです。 Cloud Composer からの GCP サービス呼び出し方法 Cloud Composer から 各 GCP サービスを呼び出すに当たり、Dataflow には Airflow に専用のクラス が存在しましたが、 Vertex AI Vector Search には存在しませんでした。そこで、 GCP の REST API ( 例:インデックス作成 API )を呼び出すことでリソースの作成や作成状況のポーリングを行うクラスをカスタムで実装しました。具体的には Airflow の Sensor クラスを利用して以下のようなイメージで実装しました(あくまで ソースコード のイメージとして簡易化したものであり、実際のものとは異なります)。 from typing import Any, Dict from airflow import models from airflow.decorators import task from airflow.sensors.base import BaseSensorOperator @ task (task_id= "create_resource" ) def create_resource (args1: int , args2: str ): """GCP のリソースを作成する(Vertex AI Vector Search のインデックスなど)。 Args: args1 (int): 引数1。 args2 (str): 引数2。 """ # call_create_resource_api 関数は別途実装。内部で GCP の REST API を呼び出す。 response = call_create_resource_api(args1, args2) return response class ResourceSensor (BaseSensorOperator): """GCP のリソース操作の状況をポーリングする。 Attributes: poke_task_id (str): ポーリング対象のタスクID。 args1 (int): 引数1。 args2 (str): 引数2。 """ def __init__ (self, *, poke_task_id: str , args1: int , args2: str , **kwargs): """GCP のリソース操作の状況をポーリングするクラスを初期化。 Args: poke_task_id (str): ポーリング対象のタスクID。 args1 (int): 引数1。 args2 (str): 引数2。 """ super ().__init__(**kwargs) self.poke_task_id = poke_task_id self.args1 = args1 self.args2 = args2 def poke (self, context: Dict[ str , Any]) -> bool : """ リソース操作の状況をポーリングして、完了したかどうかチェックする。 Args: context (Dict[str, Any]): Airflow のコンテキスト。どのような値が格納されているかは以下参照。 https://airflow.apache.org/docs/apache-airflow/stable/templates-ref.html Returns: bool: リソース操作が完了したかどうか。 """ # 作成したリソース情報(IDなど)を取得。 response = context[ "ti" ].xcom_pull(task_ids=self.poke_task_id) # call_get_resource_status_api 関数は別途実装。内部で GCP の REST API を呼び出す。 status = call_get_resource_status_api(response, args1=self.args1, args2=self.args2) # status = {"done": True} のような値と仮定 return status[ "done" ] with models.DAG( "dag_name" , schedule_interval= "0 0 * * mon" , ) as dag: create_resource_task = create_resource(args1, args2) wait_create_resource_task = ResourceSensor( task_id= "wait_create_resource" , poke_interval= 60 * 10 , timeout= 3600 * 3 , poke_task_id= "create_resource" , args1=args1, args2=args2, ) create_resource_task >> wait_create_resource_task ここで、 Airflow のコンテキストを利用してポーリング時に必要なリソース情報(ID など)を Sensor クラスで取得するようにしました。 Vertex AI Vector Search での類似度計算時にリトライ Vertex AI Vector Search で類似度計算を行う際に、 Exponential backoff アルゴリズム によるリトライ処理を入れるようにしました。理由は、実際に運用していると Vertex AI Vector Search の呼び出し時に google.api_core.exceptions.InternalServerError: 500 Failed to call Service Control Check. や google.api_core.exceptions.Unknown: None Stream removed というエラーが稀に発生することがあったためです。Vertex AI Vector Search による類似度計算は1回の定期実行あたり約 8 時間かかるため、途中で停止するとリトライにかかる時間が大きいという問題がありました。 Exponential backoff アルゴリズム によるリトライは backoff ライブラリ を利用し、リトライ対象のエラーは google.api_core.exceptions.ServerError としました。 ソースコード を見ると今回発生した google.api_core.exceptions.InternalServerError や google.api_core.exceptions.Unknown がこのクラスの子クラスであり、他の子クラスも GCP 側のサーバーエラー起因のものであるためリトライする方が良いと判断したためです。 以下が実際の ソースコード のイメージです。(簡易化したものであり、実際のものとは異なります)。 from typing import List import backoff from google.api_core.exceptions import ServerError from google.cloud.aiplatform import MatchingEngineIndexEndpoint from google.cloud.aiplatform.matching_engine.matching_engine_index_endpoint import ( MatchNeighbor, ) # エラーが解消するまでの待ち時間が不明なため、リトライの最大時間は1時間半とした。 # 類似度計算は1回の定期実行あたり約 8 時間かかるため、1時間半の待ち時間は許容する。 @ backoff.on_exception (backoff.expo, ServerError, max_time= 5400 ) def find_neighbors ( index_endpoint: MatchingEngineIndexEndpoint, deployed_index_id: str , queries: List[List[ float ]], num_neighbors: int , ) -> List[List[MatchNeighbor]]: """デプロイされたインデックスで与えられた Embedding に対して近似最近傍探索を実行。 Args: index_endpoint (MatchingEngineIndexEndpoint): Vertex AI Vector Search のインデックスエンドポイントクラス。 deployed_index_id (str): インデックスのデプロイID。 queries: List[List[float]]: Embedding のリスト。 num_neighbors (int): 近似最近傍探索で取得する Embedding 数。 Returns: List[List[MatchNeighbor]]: 類似度トップ `num_neighbors` の id と類似度のリスト。 """ return index_endpoint.find_neighbors( deployed_index_id=deployed_index_id, queries=queries, num_neighbors=num_neighbors ) 実際にこのリトライ処理を入れたことで、その後の運用時に同じエラーが発生することがありましたが、無事にリトライされることで途中で停止せずに実行完了していました。 まとめ 本記事では、類似画像検索システムの内製化におけるシステム面での課題と、それをどのように解決したかについて説明しました。大規模なデータを毎月現実的な時間で処理しなければならない課題を、複数の GCP サービスを組み合わせて高速化することで解決しました。また、複数の GCP サービスを連携してスケジュール実行する必要がある課題を、 Cloud Composer を利用して実装することで解決しました。 今後は他の機能への応用を検討しています。本記事が類似のシステムを構築されている方々の参考になれば幸いです。 明日の記事の担当は UI/UXチーム の飯沼さんです。お楽しみに。 追記:2025/01/21 第48回 MLOps 勉強会で登壇しました! 第48回 MLOps 勉強会 にてこのブログの内容を発表いたしました。勉強会ページには配信 アーカイブ も掲載されておりますため、ご興味ある方はご覧いただけますと幸いです。 speakerdeck.com エニグモ では一緒にデータを利用したサービス価値向上を実現していただけるデータサイエンティストを募集中です!世界178ヶ国に1100万人超の会員を有し、出品数は630万品を超える BUYMA には膨大なデータが蓄積されており、データ活用の余地はまだまだあります。ご興味ある方はカジュアル面談からでもお話できますと幸いです。 他の職種も絶賛募集中です! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは!フロントエンドエンジニアの張です! この記事はEnigmo Advent Calendar 2024の3日目の記事です。 エンジニアは日頃のタスクを対応するためにいろんなツール駆使していますが、絶対になくてはいけないツールと言いますと、やはり テキストエディタ ー一択だと、私は思っています。 その テキストエディタ ーですが、実は何種類もあって、側から見ると、どれも同じように見えるかもしれませんが、毎日何時間もそれで仕事をするエンジニアにとって、自分に合うエディターを選ぶことは実は仕事の効率とモチベーションに大いに関わっています。 本記事では、ここ約一年半、私が毎日使っている最強エディター Neovim について、ご紹介します。 Neovim とは もしかしたら Neovim を知らない方も多いかもしれませんが、それがエンジニアなら誰しも一回は使ったことがある、名高い Vim エディターをリファクターしたものです。 そのため、 Neovim は Vim の特性と独自の機能を備える、モダンな コマンドライン エディターになっています。 特性 Vim から受け継いだもの ベースが Vim になっている Vim を fork して開発したもの Vim motion Vim command 反応速度が早い ミニマル 独自の特性 Lua でコンフィグ可能 LSPのネイティブサポート 強み Neovim について簡単に説明しましたが、それは一体ほかの主流のエディターと比べて、何の強みがあるでしょう。 Vim Motion Vim Motionというのは Vim 、あるいは Neovim 上で使える、カーソルを移動させたり、テキストを編集したり、できるショットカットです。 Motionを使いこなせると、マウスなしでテキストを高速で編集、ナビゲートすることが可能になります。 それは日々大量なテキスト、あるいはコードを処理するエンジニアにとって、とても貴重なスキルです。 実例 個人的に最も使っている Normal Mode 下の基礎Motionが以下となります Motion 機能 [count] h カーソルを( count 行)左に移動させる [count] j カーソルを( count 行)下に移動させる [count] k カーソルを( count 行)上に移動させる [count] l カーソルを( count 行)右に移動させる [count] w カーソルを次/ count個後の単語に移動させる [count] b カーソルを前/ count個前の単語に移動させる i insert mode に移行して、カーソルを左に一行移動させる a insert mode に移行して、カーソルを右に一行移動させる d [motion] motionの終着点までの文字を削除する x カーソルの位置の文字を削除する y [motion] motionの終着点までの文字を複製する p yで複製した内容をペーストする Text object selection を利用したMotion Motion 機能 ciw カーソルが位置する単語を丸ごと削除して、insert modeに移行する diw カーソルが位置する単語を丸ごと削除する dap カーソルが位置する段落を削除する yap カーソルが位置する段落を複製する 使えるMotionは実際まだまだありますが、量が多いのもありますし、 公式ガイド を参考した方が的確なので、ここでは割愛させていただきます。 効率的で楽しい 以上の実例を見ると、 Vim / Neovim が高速なテキスト編集を実現できることがわかると思いますが、それは実は私にとって、一番大事なメリットではありません。 では、一番のメリットは何でしょうか! 楽しい! そう!楽しくテキストを編集できること自体が一番のメリットだと思います。 よく見ますと、 Vim Motionってゲームのコンボに見えないでしょうか? Vim / Neovim なら一日中コードを書いても退屈することはありません! それがエンジニアのモチベーションを引き上げて、効率を改善できることを私はこの一年半ですごく実感できました。 PDE(Personalized Development Environment) Neovim は一部のエンジニアから、 PDE 、つまり 個人的開発環境 とも呼ばれています。 それは Neovim のコンフィグ自由度がとても高くて、ユーザーがそれを思い通りに自分好みにできるからです。 具体的に言うと、UIの表示、文字を打つ時のエフェクト、ユーザー入力への反応など、その気になれば全部自分で実装できます。 結果として、ユーザーは自分のワークフローにピッタリな開発環境を構築できて、開発の効率を大幅に上げられます。 効率改善以外、PDEはユーザーのモチベーションの向上にもつながると思います。 なぜかと言うと、エンジニアが仕事のタスクをPDEで対応することで、自分が設計したツールをテストできるからです。 開発が好きなエンジニアなら、それだけで仕事へのモチベーションが大幅に上がるでしょう。 多彩な プラグイン 近年、開発を支援したり、ワークフローをスムーズにしたり、できる プラグイン がプログラミングエディターの必要不可欠な要素だと思われる傾向が強まりつつあります。 当然、 プログラマー のためのエディターである Neovim にも充実した プラグイン エコシステムがあります。 ここでは、重点的に、何個か人気な プラグイン についてご紹介します。 Telescope Telescopeは Neovim コアの機能を利用した、高性能な Lua 製 Fuzzy Finder です。 簡単に言うと、指定した範囲以内のコンテンツをキーワードで検索するツールです。 例えば、コードベース内のファイルを大まかな文字列で検索したり、特定な文字列がコードベースのどこに記載されているかを探したり、することが可能です。 それだけでも結構便利なツールなんですが、もっとすごいのはTelescopeの拡張性です。 Neovim と同様に、Telescopeは拡張性に重きを置いてデザインされました。 そのため、Telescopeの既存機能を利用して、新しい機能を作成することも比較的に簡単にできます。それに、純 Lua 製の プラグイン であるため、 Lua でカスタマイズするのもとても便利です。 もちろん、原作者と他のコミュニ ティー メンバーによって、もうすでにたくさんの 拡張機能 が作られたので、まだ自分で機能を作成したくないユーザーでも充実した既存機能を利用できます。 私がよく使っている機能をリストアップすると、以下となります。 builtin.buffer 開いたバッファの中からファイルを検索する builtin.find_files コースベース内のファイルを検索する builtin.live_grep コースベース内で特定の文字列やパターンを検索する 詳細に興味がある方はぜひ 公式GitHubページ をご覧ください! LSP(Language Server Protocol) 関連 プラグイン Neovim と Vim の違いの話になると、ビルトインのLSPサポートの有無が必ずといっていいほど話題に出されます。実際、LSPのサポートを目当てで Neovim に移行したユーザーも結構います。 では、LSPとは一体何なのでしょうか? 一言で言うと Microsoft 社が開発した、エディター/ IDE とLanguage Serverの間のコミュニケーションを可能にする プロトコル です。 Visual Studio Code などのエディターがコードのオートコンプリート、定義元への移動、コードの診断(Diagnostic) ができるのもLSPのおかげです。(LSPは元々 Visual Studio Code のために作られています) 詳細は こちらの公式サイト で閲覧できるので、興味がある方はぜひご確認ください。 前にも話した通り、 Neovim にもそのLSP機能を搭載されています。そして、それを中心に作られた プラグイン もたくさんあります。それらも利用すれば、簡単かつ自由度が高いLSP設定ができるため、 Neovim でのLSP体験はいろんなエディターの中でもトップクラスです。例えば、Language Serverから受信した情報に基づいて、画面上にコードに関する情報を表示するのも簡単にできます。 複雑だと思うかもしれませんが、以下のLSP プラグイン を利用すれば、大体のLSP設定ができます。 nvim-lspconfig いろんなLanguage Serverのデフォルトコンフィグを提供する mason.nvim Neovim 用のLanguage Serverパッケージマネージャー 簡単に主流のLanguage Serverをインストールできる mason-lspconfig nvim-lspconfig と mason を繋げるもの LSPを管理する mason. vim 今ではLSPを利用するエディターも結構増えましたが、設定の手軽さ、自由度、エコシステムの大きさのため、 Neovim を選択するユーザーもまだまだ増えています。 おわりに 以上が私が約一年半 Neovim を使って、未使用の方にも知ってほしい Neovim の見どころでした。 まだまだ説明しきれてないことが多いですが、 Neovim が素晴らしいプログラミングエディターであることが伝われたら嬉しいです! 文章を読むだけではピンと来ないかもしれないので、時間がある時、ぜひ、 Neovim を使ってみてください! 明日の記事の担当は 採用広報担当 の戸井さんです。お楽しみに。 おまけ 初心者が自分で0から Neovim を始めるのは結構ハードルが高いので、自分が最初に使ったリソースを幾つか共有したいと思います。 Neovim公式 kickstart.nvim Neovim のコア開発者の一人が開発した Neovim コンフィグ clone したらすぐ使える コードの説明がとても詳しくて、初心者に優しい vimtutor Vim Motionを勉強できる cli tool vim をインストールすれば、一緒にインストールされる Unix-like のシステムでは大体デフォルトでインストールされている
アバター
こんにちは!Webアプリケーションエンジニアの 川本 です! 最近は BUYMA の出品者向けのチームでパフォーマンス改善に取り組んでおります。 この記事は Enigmo Advent Calendar 2024 の 2日目の記事です。 少し日が経ってしまいましたが、2024年10月16に開催された「Datadog Summit Tokyo 2024」に参加してきました。 www.datadoghq.com 直近の業務でパフォーマンス改善に取り組む機会が多かったのですが、その際にオブザーバビリ ティー の向上を支えてくれる、Datadogに興味を持ちました。開発エンジニアにとってもこれらを活用できると視野が広がると感じ参加を決めました。 印象に残ったセッション Datadog ダッシュ ボードで 見える化 する、新たなビジネス価値創造のチャンス www.datadoghq.com このセッションでは、リリースした機能が意図通りに使われているかを分析し、期待されるスコアを可視化する ダッシュ ボードの活用例が紹介されました。 ダッシュ ボードから課題を発見し、改善につなげることでビジネス価値を創造していくという内容でした。 特に印象に残ったのは、エンジニアがビジネス観点を持つ重要性です。 ダッシュ ボードを作成するためには、案件の目的や必要なデータを理解する必要があり、それがビジネス観点を養うきっかけになると感じました。 弊社でも新機能をリリースする際に、エンジニアが ダッシュ ボードを準備し、ビジネス側と連携しながら価値創造に貢献していきたいです。 開発者の生産性向上 www.datadoghq.com このセッションでは、Datadogを活用した開発生産性向上に関するパネルディスカッションが行われました。 最も印象に残ったことは、 Wantedly 社の市古さんがおっしゃっていた「オブザーバビリティは開発を加速させる」という点です。 www.wantedly.com 大規模 リファクタリング やライブラリのバージョンアップ等を行った際の影響範囲はとても広いので、ステージング環境でなるべく確認しようとしても、全てを完璧に把握することは難しいことが多いかと思います。そのため、本番環境にリリースする際はどうしても慎重になりスピード感が損なわれてしまいます。 しかし、オブザーバビリティを向上させると、問題が発生しても素早く発見・対応できるという安心感が得られます。これにより、適切なリスクを取れるようになり、レビュー 工数 の削減や認知負荷の軽減につながるため、スピードと品質の両立が可能になるという内容はとても納得できました。 弊社で運営しているサービスの「 BUYMA 」も歴史の長いサービスで、これから大規模リプレイス、大規模 リファクタリング 、ライブラリの大幅なバージョンアップを控えております。その際に今回学んだことを活かして、オブザーバビリティを向上させながら開発生産性を維持しながら問題と向き合っていきたいです。 導入したいDatadogの機能 今回のサミットを通して様々なDatadogの機能について知ることができました。 その中でも自社で導入したいと考えている機能は Datadog Continuous Profiler です。 www.datadoghq.com Datadog Continuous Profilerとは? Datadogの公式ページでは以下のように紹介されております。 Datadog Continuous Profiler を使用すると、最小限のオーバーヘッドでスタック全体にわたって本番環境でのコードパフォーマンスを分析できます。コードプロファイリングを利用して、アプリケーションで最もリソースを消費するメソッドまたはクラスをすばやく検出および最適化できます。これにより、コードの効率が向上し、 クラウド プロバイダーのコストが削減されます メソッド単位でどれだけ時間がかかっているかや、CPU時間、メモリ使用量等のリソースの消費量が可視化されるので、パフォーマンスの ボトルネック を発見するのに最適です。 導入したい理由 この機能を導入したいと思ったのは、弊社で運用されはじめて長年経過した機能のパフォーマンス改善に取り組んだことがきっかけでした。 私は ボトルネック を見つけるのにまず該当するコード全体を読んでいたのですが、これは根拠のない推測をしているだけで効率が悪かったなと反省しております。 そういった際に Continuous Profiler で計測してコードレベルで可視化して ボトルネック になっている箇所を明確にし、修正の目処を立てるといったことができていればより効率的かつ効果的なアプローチがとれていたなと感じております。 おわりに 今回のイベントを通してDatadog、オブザーバビリティに関して新たな知見を得ることができたのと同時によりこの分野に興味を持つことができました。 今回ワークショップには参加できなかったので Datadog Learning Center で興味のある講座を受講して実際に手を動かしてDatadogに関する知見を深めていこうと思います。 最後に、帰り際にいただいたDatadogのかわいいグッズも大切に使わせていただきます! 運営の皆様、素晴らしいイベントをありがとうございました! 明日の記事の担当は フロントエンドエンジニア の張さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは!WEBアプリケーションエンジニアの 川本 です! この記事はEnigmo Advent Calendar 2024の1日目の記事です。 弊社の運営する BUYMA では、社内システムよりタイムセールを毎週 約100万商品 に設定しています。しかし従来のシステムでは、この処理に 約100時間 もかかっており、運用負荷が大きな課題となっていました。本記事では、パフォーマンス改善によって処理時間を 約5時間 に短縮し、運用効率を向上させた事例をご紹介します。 タイムセール設定機能について 処理フロー タイムセール設定機能はざっくり以下のような処理フローになっています。 CSV ファイルのアップロード タイムセール設定依頼情報を記載した CSV ファイルを S3 にアップロードします。 SQSへのエンキュー S3にファイルがアップロードされると、SQSにメッセージがエンキューされます。 Redisに保存 常駐しているデーモンがSQSキューをポーリングし、SQSから取得したメッセージをRedisに保存します。 Sidekiqジョブの実行 Redisに保存されたメッセージをSidekiqがデキューして、タイムセール設定の処理を実行します。 性能 タイムセール設定を行うSidekiqの性能は以下の通りです。 専用プロセスの使用 タイムセール設定ジョブは専用のSidekiqプロセスで実行。 並列処理数 1プロセスあたりの並列処理数は10。 冗長化 2台のサーバー構成で冗長性を確保。 設定例(sidekiq.yml) --- :labels : - default :concurrency : 2 :pidfile : tmp/pids/sidekiq.pid :logfile : ./log/sidekiq.log production : :concurrency : 10 :queues : - [ setting_timesale, 1 ] 課題の発見 従来の仕組みでは、タイムセール設定依頼の CSV ファイル1つを1スレッドで処理していました。これにより、以下のような問題が発生していました。 スレッド活用不足 Sidekiqはマルチスレッド対応で高い並列処理性能を持っていますが、1つのスレッドが1つの CSV を丸ごと処理していたため、マルチスレッドであること効果的に活用できていませんでした。 長時間実行による運用負荷 1つの CSV には約100万件の商品データが含まれており、これを1スレッドで処理することで、処理時間が長時間に及び、運用上現実的でない時間になっていました。 CSV 分割の運用負荷 スレッドを有効活用するには、 CSV をあらかじめスレッドの数だけ手動で分割する必要があり、これが運用上の手間となっていました。 改善アプローチ 上記の課題を解決するため、システム側で1つの CSV データをバッチ単位(100商品)で分割し、各バッチを複数スレッドで並列処理する方式に変更しました。これにより、処理の効率化と運用負荷軽減を同時に実現することが可能になりました。 改善後の処理の流れ 親ジョブ が CSV データをバッチ単位に分割し 子ジョブ を作成・エンキューする。 子ジョブ が実際のタイムセール設定処理を実行する。 監視ジョブ が 子ジョブ の進捗を追跡し、全て完了したら設定完了の処理を実行する。 登場してきたジョブについて↓ 親ジョブ CSV ファイルから取得したタイムセール設定依頼データをバッチ単位に分割。 各バッチについて 子ジョブ をエンキューし、ジョブIDを記録。 最後に 監視ジョブ をエンキューし、 子ジョブ の進捗を監視。 実装例↓ module SidekiqWorker # 親ジョブ class Parent include Sidekiq :: Worker sidekiq_options queue : :setting_timesale BATCH_SIZE = 100 def initialize (args) # 省略 end def perform child_job_ids = [] # バッチ単位でタイムセール依頼データを分割 timesale_request_data.each_slice( BATCH_SIZE ) do |batch| # バッチ単位でタイムセール設定を行う 子ジョブ をエンキュー child_job_ids << SidekiqWorker :: Child .perform_async(batch) end # 子ジョブの進捗状況を管理する 監視ジョブ をエンキュー SidekiqWorker :: Monitoring .perform_async(child_job_ids) end private # s3からタイムセール設定依頼データを取得 def timesale_request_data # 省略 end end end 子ジョブ バッチ単位(100件)の設定依頼に対してタイムセール設定の処理を実行。 ジョブの進捗状況をRedisに記録。 子ジョブの状態管理に sidekiq-status というgemを使用しました。 github.com sidekiq-status は、Sidekiqで実行中のジョブの状態を追跡するためのGemです。ジョブの状態(例: queued, working, completeなど)をRedisに保存し、進捗をリアルタイムで確認できるようになります。 module SidekiqWorker # 子ジョブ class Child include Sidekiq :: Worker include Sidekiq :: Status :: Worker sidekiq_options queue : :setting_timesale # 24時間ジョブの状態をRedisに保持する def expiration @expiration ||= 60 * 60 * 24 end def perform (timesale_request_data) SetTimeSaleService .new(timesale_request_data).call end end end 監視ジョブ 子ジョブの進捗を追跡し、全ての子ジョブが完了した場合に完了処理を実行 追跡できない子ジョブが存在した場合はアラートを飛ばす。 sidekiq-statusの API よって以下のように子ジョブの状態を確認することができます。 job_id = SidekiqWorker :: Child .perform_async # :queued, :working, :complete, :failed or :interrupted, nil after expiry status = Sidekiq :: Status ::status(job_id) # <- ジョブ状態を確認 Sidekiq :: Status ::queued? job_id # <- キューにあるか? Sidekiq :: Status ::working? job_id # <- 実行中か? Sidekiq :: Status ::retrying? job_id # <- リトライ中か? Sidekiq :: Status ::complete? job_id # <- 完了したか? Sidekiq :: Status ::failed? job_id # <- 失敗したか? Sidekiq :: Status ::interrupted? job_id # <- 中断されたか? 実装例↓ module SidekiqWorker # 監視ジョブ class Monitoring include Sidekiq :: Worker include Sidekiq :: Status :: Worker sidekiq_options queue : :setting_timesale SLEEP_TIME = 15 def perform (job_ids) @job_ids = job_ids loop do @job_ids .reject! do |job_id| job_complete?(job_id) end break if @job_ids .empty? sleep SLEEP_TIME end TimeSaleSettingCompletionService .new(args).call end private # ジョブが完了したか? def job_complete? (job_id) Sidekiq :: Status .complete?(job_id) end end end 学んだこと 性能要件の見直しの重要性 長年運用されているシステムは、リリース当時の性能要件がそのまま適用されていることが少なくありません。しかし、システムの利用状況や運用環境は時間の経過とともに変化します。今回の事例でも、リリース当初は妥当だった処理速度が、現在では運用負荷を引き起こす大きな要因になっていました。 そこで重要なのは、現状の運用方法をしっかりと把握し、必要に応じて性能要件を再定義することです。運用者に ヒアリ ングを行い、現在の問題点を明確にすることで、改善に向けた具体的な指針を得ることができます。 並列化によって増す複雑性 並列化はシステムの処理速度を大幅に向上させる一方で、複雑性を増す側面があります。一連の処理が複数のジョブに分散されるため、それぞれのジョブの状態を適切に管理する必要が生じます。今回の事例でも、Sidekiqを用いた並列化に伴い、以下のような課題が明らかになりました。 ジョブの状態管理コスト 並列化することで、ジョブの進捗や完了状態を追跡する仕組みが必要になります。このため、Redisを活用したジョブの状態管理が不可欠となりましたが、それには追加の開発コストと運用リスクが伴います。 ジョブの状態欠損のリスク 例えば、Redisに障害が発生した場合、一部のジョブの状態が欠損する可能性があります。このリスクを考慮し、ジョブの再実行や障害時の リカバリ ープロセスを検討する必要があります。 並列化の インパク トは大きいですが、システムの複雑性は増してしまいます。まずは 処理内容自体のパフォーマンス改善 に目を向けることが大切です。今回のプロジェクトでは、並列化に先立ち、子ジョブ内で実行されるタイムセール設定処理の最適化を行いました。 おわりに パフォーマンス改善は、単に処理を「速くする」ことが目的ではなく、システムの要件や複雑性を考慮し、最適なバランスを見つけることが重要だなと思いました。 今回学んだことを活かしてこれからもパフォーマンス改善に取り組んでいきたいです! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、データサイエンティストの髙橋です。業務では企画/分析/ 機械学習 モデル作成/プロダクション向けの実装/効果検証を一貫して行っています。 この記事では類似画像検索システムを内製化したことで、既に社内で利用していた他社製のものと比較して精度を維持しながらコスト削減が実現できたことについて紹介します。 なお、類似画像検索システムの 機械学習 部分と システム開発 部分( GCP を利用した処理の高速化)との2つに分けて紹介します。今回は前者の 機械学習 部分についての記事となります(後者の システム開発 部分の記事は こちら )。 内製化の目的 内製化による事業インパクト 内製化成功のポイント BUYMA における類似画像検索の課題 類似画像検索方法の概要 類似画像検索各ステップの詳細 商品画像内の物体存在箇所をセグメンテーションし切り抜き 切り抜いた画像の Embedding 計算 Embedding により画像同士の類似度計算 画像ハッシュにより画像同士の類似度計算 Embedding 類似度と画像ハッシュ類似度を組合せ同一商品の可能性判定 まとめ 内製化の目的 弊社が運営している CtoC EC サービス BUYMA は、CtoC という特性上同じ商品が異なる出品者から出品されている場合が多いです。そのため、どの商品が今売れ筋なのか、その商品のサイト全体での在庫状況はどうなのか、価格差はどの程度あるのかなどを正確に把握するには異なる出品者から出品されている同じ商品の 名寄せ が必要です。 それを実現するために、商品画像同士の類似度を計算し一定以上のものを人手で アノテーション することで商品の 名寄せ を社内で行っていました。ここで商品画像同士の類似度を計算するために他社製の類似画像検索システムを利用していました。しかし、その費用がそれなりに高く、内製化することでコスト削減を実現したいというのが目的でした。 内製化による事業 インパク ト 内製化したシステムは既に稼働しており、これまでと比較して精度は維持したまま年間数百万円規模のコスト削減が見込まれています。 加えて、他社製の類似画像検索システムではコストが高く断念していた EC サイトでの購入者向けの画像を起点にしたレコメンドや、画像で類似商品を検索できる機能なども検討出来るようになりました。 内製化成功のポイント 以下2点が内製化成功のポイントであったと考えています。 ト レーニン グ不要で十分な精度が出る 機械学習 モデルの登場を待ち、それを利用したこと 精度と実現スピードのバランスを考えビジネスで活用できるレベルの結果を素早く出したこと 1点目について、私の入社前から社内では 名寄せ に対する要望があり 機械学習 モデル開発を検討したことがあったようですが、 BUYMA には数百万件以上の膨大な商品があり 機械学習 モデルのト レーニン グ・評価の イテレーション に時間がかかる上に中々期待する精度までは至らずストップしている状態でした。 そこで、どうにかして精度を上げる方法は検討せず、世の中では大規模な深層学習モデルの開発が活発であったため、何か利用できそうなものがないか定期的に調査していました。すると、 Contrastive Language-Image Pre-Training (CLIP) や Segment-Anything Model (SAM) のようなト レーニン グ不要で高精度に画像の類似度計算やセグメンテーションが出来るモデルが登場し、このタイミングであれば時間をかけすぎずに内製化出来るのではないかと思い PoC をスタートさせました。 2点目については、精度が期待する水準に至らない場合に、 Fine-Tuning のような時間がかかることよりも先に、後述する簡易な画像処理 アルゴリズム を利用して運用上問題ない精度まで向上させました。これにより、この PJ はほぼ1人での担当でしたが企画から本番運用まで約1年で完遂することが出来ました。おそらくですが、あまり時間をかけすぎてしまうとビジネス上の課題の優先度の変化などから ペンディング する必要性が生じた可能性が高かったと考えています。 BUYMA における類似画像検索の課題 具体的にどのような方法で類似画像検索を実現したかの前に、 BUYMA における類似画像検索の課題について説明します。 BUYMA の商品画像は出品者の方が自由に設定できるものであり、商品単体よりも様々なものをコラージュしている場合が多いです。例えば、色違い/角度違いの商品、着用画像や文字などをコラージュしていることが多いです。 そのため、単純に何らかの手法で画像同士の類似度を計算しても、同じ商品の画像にも関わらず類似度が低く出てしまう課題がありました。 類似画像検索方法の概要 課題に対応するために、画像から商品部分を切り抜いてその部分同士で画像類似度を計算する方法を考えました。はじめは Object Detection が出来る深層学習モデルをト レーニン グし、それにより商品部分を検知し切り抜く方法を考えましたが、 BUYMA には様々な商品がありラベル作成の手間がかかる懸念がありました。 そこで、企画検討当時に発表された Segment-Anything Model (SAM) というト レーニン グ不要で画像から物体をセグメンテーション出来るモデルの利用を検討しました。詳細は後述しますが、 SAM と画像ハッシュを組み合わせることで既存の他社製の類似画像検索システムと同等の精度を実現しました。 類似画像検索各ステップの詳細 類似画像検索は以下のステップに分かれています。 商品画像内の物体存在箇所をセグメンテーションし切り抜き 切り抜いた画像の Embedding 計算 Embedding により画像同士の類似度計算 画像ハッシュにより画像同士の類似度計算 Embedding 類似度と画像ハッシュ類似度を組合せ同一商品の可能性判定 ここで、Embedding とは以下のように画像を数値の列に変換したものです。 似た画像同士が何らかの類似度計算関数(cos 類似度など)で高い値を持つようにト レーニン グされた 機械学習 モデルを利用することで、画像同士の類似度を計算することが出来ます。 各ステップの詳細について説明します。 商品画像内の物体存在箇所をセグメンテーションし切り抜き 課題でも述べた通り、 BUYMA の商品画像はコラージュされている場合が多く、例えば以下のような同じ商品であるが2つの異なる出品者の画像をそのまま Embedding 化し類似度を計算すると 0.496 となり、低い類似度となってしまいました。 そこで、SAM というモデルを利用して商品画像内から商品を含む物体存在箇所をセグメンテーションし切り抜きました。SAMとは、あらゆる物体をセグメンテーションするための汎用的なモデルであり、さまざまな画像に対して物体を抽出することが可能です。大量のデータで訓練されており、Fine-Tuning なしでも高い精度を発揮します。 実際にSAM を利用してそれぞれの画像から物体部分を切り抜くと以下のような画像群が作成されました。この中で黒の財布部分を切り抜けている画像をそれぞれ Embedding し類似度を計算すると0.975 と非常に高い値となりました。 切り抜いた画像の Embedding 計算 前ステップで商品画像から切り抜かれた各画像について、 機械学習 モデルを利用し Embedding を計算します。ここでもラベル作成やト レーニン グの手間を削減するために、事前学習済みの深層学習モデルを利用しました。 事前学習済みモデルとしては、 Japanese-CLIP(Contrastive Language-Image Pre-Training) を利用しました。 CLIP とは、大量の画像とテキストの対応関係を学習することにより、画像と 自然言語 の両方での検索や類似度計算が可能なモデルです。このモデルを選んだ理由は、PoC で実際の BUYMA 画像で精度を検証してみて問題なさそうであったのと、日本語での 自然言語 による画像検索の拡張性も考慮してのことです(CLIP ベースのモデルであるため、 自然言語 と画像の類似度も計算可能です)。 しかし、ある程度開発が進んだ段階で open_clip のいくつかの学習済みモデルと比較した結果、より精度が良いモデルがあり場合によってはそちらを採用しても良かったかもしれないです。今後、さらに精度向上が必要な場合には再度検証を行う予定です。 Embedding により画像同士の類似度計算 前ステップにより1商品画像に対して複数の Embedding が作成されます。画像同士の類似度を1つの値として算出するために、商品内の切り抜かれた部分の Embedding の全ての組み合わせの類似度を計算しその中の最大の値を画像同士の類似度としました。 前の説明で用いた商品画像を例とすると、以下のように切り抜かれた部分同士の全ての組み合わせの類似度を計算し、その中の最大の値(ここでは黒の傾いていない財布部分同士の類似度が 0.975 で最大であった)をもとの商品画像同士の類似度としました。 この方法を試してみた時点で、既存の他社製の類似画像検索システムに近い精度が出せましたがやや及ばない状態でした。そこで後述する画像ハッシュによる類似度計算も組み合わせることとしました。 画像ハッシュにより画像同士の類似度計算 前述したように Embedding よる類似度計算では既存のシステムに精度がやや及ばない状態であり、その主な原因は商品内の一部分での切り抜き同士で過剰に類似度が高くなってしまうパターンが主でした。 具体的な例としては、以下のように商品の一部と背景部分のみを切り抜いてしまい、それら画像同士の類似度が 0.9 以上と高くなるケースがありました。 これを防ぐために、Embedding による画像同士の類似度が一定以上である組み合わせについて、画像ハッシュによる類似度も計算しました。画像ハッシュとは、画像の視覚的特徴を数値化し簡略化した表現で画像を比較する手法です。これにより、細部の違いや大まかな構造の類似性を把握できます。例えば、 Average Hash (aHash) では大まかに言うと画素値が平均よりも高いかで2値化して比較を行います。 実際に上記のようなケースにおいて画像ハッシュによる類似度を計算すると非常に高い値(画像ハッシュでは値が高いほど類似度が低い)となりました。 Embedding 類似度と画像ハッシュ類似度を組合せ同一商品の可能性判定 Embedding による類似度と画像ハッシュによる類似度を組み合わせた しきい値 を設定し、それを超えた商品画像同士のペアを同一商品である可能性が高いと判定するようにしました。既存の他社製のものでも同様に類似度の しきい値 が一定以上を同一商品の可能性が高いとしていました。 しきい値 は、既存のものと判定数をおおよそ同じにした方が移行後の運用がしやすいと考え、そうなるように決めました。また、このとき既存のものとの比較も行い精度に大きな乖離がないことを確認しました。具体的には、 しきい値 調整用のデータ(約704億の画像ペア)とテスト用のデータ(約1785億の画像ペア)を用意し、既存のものでの判定結果を正としたときに Precision 、 Recall を計算したところ以下の値でした。 Precsion Recall しきい値 調整用のデータ 0.7 0.72 テスト用のデータ 0.8 0.77 Precision 、 Recall を悪化させる要因である既存システム/内製化システムでのみ同一の可能性が高いと判定された画像ペアをそれぞれ目視で確認してみると、既存システムでのみ同一と判定されたペアにも実際には異なる商品同士があったり、内製化システムでのみ判定されたペアには実際に同一であるペアがあったりし、既存の外部ツールでも誤検知や見逃しが多くありました。 また、人手で アノテーション いただく方々に既存システムと内製化システムを一定期間並行運用していただき、運用上の精度も問題ないことを確認いただいたため、現在内製化システムに移行しています。 まとめ 内製化により精度を維持しながら大幅なコスト削減を実現し、さらなる類似画像検索機能の活用可能性も広がりました。 次回は、 GCP のマネージドサービスを利用して推論方法の各ステップを高速化した システム開発 部分の詳細を紹介する予定です。実運用では数十〜数千億規模の画像の組合せについて定期的に類似度計算する必要があり、高速化も非常に重要なポイントでした。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、 エニグモ 嘉松です。 BUYMA のプロモーションや マーケティング を担当している事業部に所属しており、その中のデータ活用推進室という部署で会社のデータ活用の推進や マーケティング ・オートメーションツール(MAツール)を活用した販促支援、 CRM などを担当しています。 さて、SELECT文で得た結果のデータを(そのまま、直接)テーブルに挿入する INSERT SELECT や、SELECT文で得た結果からテーブルを作成して更にデータまで挿入する CREATE TABLE AS は何かと便利な機能(文法、技?)ですが、 WITH句 (SELECT文による結果を一時的に名前を付けてテーブルのように利用する便利な機能(文法、技?))と併用、一緒に利用しようとした時に、ちょっとした注意点があるので備忘録として記載しておきたいと思います。 例えば、以下のようなにWITH句で複数の(仮想的な)テーブルを定義して、それらを結合(JOIN)して結果を得るような SQL があったとします。(サンプルなのでとてもシンプルな SQL にしていますが、通常では WITH句 を使う場合はもっと複雑な SQL になることが多いと思います。) ※以下の SQL はBigQueryで検証していますので、他の DBMS では異なる結果やエラーになる場合があることをご了承ください。 WITH emp AS ( SELECT 7369 AS empno, ' SMITH ' AS ename, 20 AS deptno, UNION ALL SELECT 7499 AS empno, ' ALLEN ' AS ename, 10 AS deptno, UNION ALL SELECT 7521 AS empno, ' WARD ' AS ename, 30 AS deptno, ), dept AS ( SELECT 10 AS deptno, ' DEVELOPMENT ' AS dname, ' MAYNARD ' AS loc UNION ALL SELECT 20 AS deptno, ' SALES ' AS dname, ' HOUSTON ' AS loc UNION ALL SELECT 30 AS deptno, ' RESEARCH ' AS dname, ' PALO ALTO ' AS loc ) SELECT e.empno, e.ename, e.deptno, d.dname, d.loc FROM emp e JOIN dept d ON e.deptno = d.deptno ; この SQL の結果は以下のようになります。 empno ename deptno dname loc 7369 SMITH 20 SALES HOUSTON 7499 ALLEN 10 DEVELOPMENT MAYNARD 7521 WARD 30 RESEARCH PALO ALTO この SQL の結果を INSERT SELECT でテーブルに挿入しようとした時に、うっかり以下のような SQL を書くとエラーになります。 WITH emp AS ( SELECT 7369 AS empno, ' SMITH ' AS ename, 20 AS deptno, UNION ALL SELECT 7499 AS empno, ' ALLEN ' AS ename, 10 AS deptno, UNION ALL SELECT 7521 AS empno, ' WARD ' AS ename, 30 AS deptno, ), dept AS ( SELECT 10 AS deptno, ' DEVELOPMENT ' AS dname, ' MAYNARD ' AS loc UNION ALL SELECT 20 AS deptno, ' SALES ' AS dname, ' HOUSTON ' AS loc UNION ALL SELECT 30 AS deptno, ' RESEARCH ' AS dname, ' PALO ALTO ' AS loc ) INSERT dataset.emp_dept -- 最終的な結果を得るSELECTの直前に記載 SELECT e.empno, e.ename, e.deptno, d.dname, d.loc FROM emp e JOIN dept d ON e.deptno = d.deptno ; BigQueryでは以下のようなメッセージが表示されます。 Syntax error: Unexpected keyword INSERT at [39:1] 最終的に結果を得るSELECT文の前に INSERT を記載するという、ごく自然な、直感的な、あたかも正しそうな方法ですが、エラーとなります。 正しくは、以下のようにWITH句の前に INSERT を記載する必要があります。 INSERT dataset.emp_dept -- WITH句の前にを記載する必要がある WITH emp AS ( SELECT 7369 AS empno, ' SMITH ' AS ename, 20 AS deptno, UNION ALL SELECT 7499 AS empno, ' ALLEN ' AS ename, 10 AS deptno, UNION ALL SELECT 7521 AS empno, ' WARD ' AS ename, 30 AS deptno, ), dept AS ( SELECT 10 AS deptno, ' DEVELOPMENT ' AS dname, ' MAYNARD ' AS loc UNION ALL SELECT 20 AS deptno, ' SALES ' AS dname, ' HOUSTON ' AS loc UNION ALL SELECT 30 AS deptno, ' RESEARCH ' AS dname, ' PALO ALTO ' AS loc ) SELECT e.empno, e.ename, e.deptno, d.dname, d.loc FROM emp e JOIN dept d ON e.deptno = d.deptno ; 同じように CREATE TABLE AS においても、WITH句の前に CREATE TABLE AS を指定する必要があります。 CREATE TABLE dataset.emp_dept AS -- WITH句の前にを記載する必要がある WITH emp AS ( SELECT 7369 AS empno, ' SMITH ' AS ename, 20 AS deptno, UNION ALL SELECT 7499 AS empno, ' ALLEN ' AS ename, 10 AS deptno, UNION ALL SELECT 7521 AS empno, ' WARD ' AS ename, 30 AS deptno, ), dept AS ( SELECT 10 AS deptno, ' DEVELOPMENT ' AS dname, ' MAYNARD ' AS loc UNION ALL SELECT 20 AS deptno, ' SALES ' AS dname, ' HOUSTON ' AS loc UNION ALL SELECT 30 AS deptno, ' RESEARCH ' AS dname, ' PALO ALTO ' AS loc ) SELECT e.empno, e.ename, e.deptno, d.dname, d.loc FROM emp e JOIN dept d ON e.deptno = d.deptno ; 考え方としては、 WITH句はあくまでもSELECT文の一部である。(WITH句も含めてSELECT文) INSERT SELECT 、 CREATE TABLE AS の後にはSELECT文を記載する必要があるので、その SQL の一部であるWITH句も同じく INSERT SELECT 、 CREATE TABLE AS の後に記載する必要がある。 とうことでしょうか。 以上です。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、インフラグループ Kubernetes チームの福田です。 突然ですが、Webアプリケーションでユーザの認証にOIDCを使うことはよくあると思います。 弊社でも様々な箇所でOIDCが利用されてます。 自社で開発しているWebアプリケーションや最近のログイン機能を持つ OSS の多くは、OIDC Providerさえ用意すればOIDCを利用することができます。 しかし、現実的にはログイン機能を持たない OSS のWebアプリケーションでOIDC認証を使いたいケースや自前で開発したWebアプリケーションにおいてもわざわざOIDCのクライアント機能を追加実装するのが面倒なケースがあります。 そんな時に使えるのがOAuth2Proxyです。 OAuth2Proxyはリバースプロキシとして動作しながら、OIDCの認証をしてくれます。 具体的にはクライアントからのアクセスに対してOIDCの認証を行い、認証されたクライアントからのアクセスのみをバックエンドに通過させるといったことが可能です。 サンプル構成の構築 サンプル構成を通して Kubernetes 上での構築方法を紹介していきます。 サンプル構成ではOAuth2Proxyのバックエンドにnginxを利用し、OIDCプロバイダにはoktaを使いたいと思います。 okta(OIDC Provider)の設定 oktaのApplicationを作成します。 Sign -in methodは OIDC を選択し、Application typeとして Web を選択します。 Grant typeでは Authorization Code と Refresh Token を選択します。 Login Redirect URIs はお使いの環境に合わせて設定してください。(ここでは https://corp.example.com/oauth2/callback としておきます。) また、作成後に生成された Client ID と Client Secret をメモしておきます。 最後にユーザのアクセス許可設定をします。 シークレット情報の作成 シークレット情報をSecretとして保存します。 kind : Secret apiVersion : v1 metadata : name : my-credential type : Opaque data : client_id : ********** client_secret : ********** cookie_secret : ********** client_id と client_secret は"okta(OIDC Provider)の設定"のところでメモした値を使います。 cookie_secret にはランダムな値を使います。 Podの作成 OAuth2Proxyとnginxを構築します。 apiVersion : apps/v1 kind : Deployment metadata : labels : app : sample name : sample spec : replicas : 1 selector : matchLabels : app : sample template : metadata : labels : app : sample spec : containers : - name : redis image : redis volumeMounts : - name : cache mountPath : /data - name : nginx image : nginx - name : oauth2-proxy image : bitnami/oauth2-proxy ports : - name : oauth-proxy containerPort : 80 args : - --http-address - 0.0.0.0:80 env : - name : OAUTH2_PROXY_UPSTREAMS value : http://localhost/ - name : OAUTH2_PROXY_PROVIDER_DISPLAY_NAME value : okta - name : OAUTH2_PROXY_PROVIDER value : oidc - name : OAUTH2_PROXY_OIDC_ISSUER_URL value : https://sample.okta.com/oauth2/default - name : OAUTH2_PROXY_CLIENT_ID valueFrom : secretKeyRef : name : my-credential key : client_id - name : OAUTH2_PROXY_CLIENT_SECRET valueFrom : secretKeyRef : name : my-credential key : client_secret - name : OAUTH2_PROXY_PASS_ACCESS_TOKEN value : 'true' - name : OAUTH2_PROXY_EMAIL_DOMAINS value : '*' - name : OAUTH2_PROXY_REDIRECT_URL value : https://corp.example.com/oauth2/callback - name : OAUTH2_PROXY_COOKIE_SECURE value : 'false' - name : OAUTH2_PROXY_COOKIE_SECRET valueFrom : secretKeyRef : name : my-credential key : cookie_secret - name : OAUTH2_PROXY_SKIP_PROVIDER_BUTTON value : 'true' - name : OAUTH2_PROXY_COOKIE_NAME value : SESSION - name : OAUTH2_PROXY_COOKIE_SAMESITE value : lax - name : OAUTH2_PROXY_SESSION_STORE_TYPE value : redis - name : OAUTH2_PROXY_REDIS_CONNECTION_URL value : redis://localhost startupProbe : initialDelaySeconds : 5 periodSeconds : 5 tcpSocket : port : 6379 volumes : - name : cache emptyDir : {} nginxのPodに対して サイドカー としてOAuth2Proxyコンテナを差し込んでいます。 redisコンテナがありますが、これはOIDCプロバイダーへのアクセスを減らすためのセッション情報のキャッシュとして使ってます。 サンプル構成の完了 Ingress やServiceなどのリソース作成の説明は省略しますが、これで構築は完了です。 対象のURL(この記事の場合は https://corp.example.com )にアクセスするとoktaのサインイン画面が表示されます。 そして、サインインして認証をPASSするとnginxにアクセスできます。 参考リンク OAuth Provider Configuration | OAuth2 Proxy Add Auth to Any App with OAuth2 Proxy | Okta Developer まとめ 今回はOAuth2Proxyについて紹介させていただきました。 本記事を通して構築がとても簡単であることが分かったと思います。 OAuth2Proxyのようなツールを自前で社内開発しているところも多いのではないでしょうか。 メンテナンスコストの理由からそういった社内開発のアプリを OSS へ移行することを検討している場合はOAuth2Proxyは選択肢の1つとしてありかと思います。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
article p { text-align: justify; } .entry-content h3 { font-size: 140%; } Hello, this is Fernand from the Global Buyma Team. As an engineer, I am responsible for maintaining the English version of Buyma . I recently had the opportunity to attend an AI conference along with my manager. I’m excited to share our experiences and insights with you. So fasten your seatbelt, relax, and enjoy the journey through the world of AI that we embarked upon. Merhaba! On May 10-11, 2024, I had the privilege of immersing myself in the vibrant intersection of technology and tradition at the two-day AI conference organized by Mad Street Den in Istanbul. This city, which uniquely straddles two continents—Europe and Asia—offered the perfect backdrop for an event that brought together global leaders to explore the forefront of artificial intelligence across various sectors. AI on Global Buyma Global Buyma employs the capabilities of Vue.ai's AI service to curate the most suitable product images for display in search results. The AI evaluates and identifies the best quality image from those uploaded by sellers to represent each product. Bridging Cultures, Advancing Technology The conference kicked off with an insightful tour around Istanbul's historic centre, embodying the perfect blend of cultural heritage and modernity. Then we headed to the Sarnıç, a 1500-year-old cistern lying at the heart of Istanbul, which set the stage for an evening of engaging dialogue. Over dinner, we forged meaningful connections with fellow attendees, delving into spirited discussions about the transformative potential of AI and sharing diverse perspectives on its future impact. ・Dinner at The Sarnıç. Honoring the special guests of the event. Key Highlights from the Conference The following day, we journeyed to the magnificent Sait Halim Pasha Mansion, where leaders from diverse sectors, including pharmaceuticals, e-commerce, and logistics, shared their insights on implementing AI within their industries. Ashwini Asokan and Anand Chandrasekaran, the founders of Mad Street Den, provided a comprehensive overview of how their tools are automating the way businesses operate, shifting the focus from infrastructure to innovation. Then, it was followed by a thought-provoking panel discussion that shed light on the banking industry's cautious stance on migrating to cloud services, highlighting the need for policy changes and compliance measures to ensure security. And much wide array of topics, extending from Zenyum's innovative application of AI in dental health assessments to the exciting reveal of Vue.ai's advanced personalization engine, designed to elevate the shopping experience. Their presentations vividly demonstrated the revolutionary capabilities and the expansive future of AI technologies. ・Panelists delved into AI's transformative impact and its common use cases ・Timo Weiss, Nithya Subramanian, and Sandal Kakkar engaged in a compelling discussion about "Gen AI: Existential Crisis or Competitive Advantage?" Evening Soirée: A Sunset to Remember After absorbing a plethora of information, it was time to let loose. The soirée, set against a breathtaking Istanbul sunset, provided a relaxed atmosphere for us to unwind, dance, and revel in the sumptuous dinner prepared for us. ・Turkish performing their traditional dance, Sufi whirling Looking Ahead for Global Buyma Discussions about these AI applications sparked ideas for enhancing the Global Buyma system, such as implementing auto- tagging based on product page content. Additionally, the personalization engine where AI analyzes shoppers' interactions would be perfect, allowing for dynamically tailored product displays and recommendations to cater to individual preferences. However, some guests noted that the use of AI for language translation in page displays requires further refinement.   Bye for now! ・From right: Ashwini Asokan, CEO & Founder of Mad Street Den, Hibaru Maywood, Head of Global Buyma 株式会社 エニグモ すべての求人一覧
アバター
Argo Workflowsを使ったPersistentVolumeの定期バックアップ こんにちは。 インフラグループ Kubernetes チームの福田です。 今回はPV(PersistentVolume)の定期バックアップシステムについて紹介したいと思います。 PVのバックアップについて PVのバックアップといっても色々とありますが、本記事ではスナップショットの取得を意味します。 スナップショットの取得は CSI が対応していれば、 external-snapshotter を利用することで、CustomResourceを書くだけで実現できます。 extenal-snapshotterを全く知らない方は以下の AWS の記事が概要を掴む参考になるかと思います。 aws.amazon.com バックアップシステムの仕組み 概要 システムの概要は以下のようになっています。 Argo Workflowsが定期的にVolumeSnapshotリソースを作成し、それをGitLabへプッシュする。 GitLabのCIが マニフェスト をバリデーションする。 Argo CDが マニフェスト のあるコード リポジトリ の変更を検出して、それを Kubernetes にデプロイする。 詳細 定期的に実行する処理 定期的な処理の実行はCronWorkflowで行っています。 単にCronJobとせず、CronWorkflowとした理由は、一度だけの実行や、定期処理の一時停止などが GUI で簡単にできるからです。 VolumeSnapshotリソースの生成 VolumeSnapshotリソースの生成を行うアプリケーションは Golang で実装した自前のコンテナアプリになります。 コンテナアプリは前述したCronWorkflowで実行されます。 世代管理機能 コンテナアプリはVolumeSnapshot マニフェスト を生成すると共に、古いVolumeSnapshot マニフェスト を削除するローテーション機能を持っています。 何世代分までのVolumeSnapshot マニフェスト を維持するかは 環境変数 で指定可能になっています。 VolumeSnapshot マニフェスト 間の新旧の比較はVolumeSnapshot マニフェスト 生成時のタイムスタンプを独自の アノテーション に記載し、それを比較することで実現しています。 管理対象フラグ ローテーション管理の対象とする否かのフラグを表現する独自の アノテーション もあります。 原則、コンテナアプリで生成されたVolumeSnapshotはこのフラグが立っています。 手動で作成したVolumeSnapshotやコンテナアプリによって作成された永続的に残しておきたいスナップショットについては、このフラグを下ろすことでローテーションの管理対象から外れて自動で削除されないようにできます。 生成される マニフェスト のサンプル apiVersion : snapshot.storage.k8s.io/v1 kind : VolumeSnapshot metadata : name : SAMPLE_NAME namespace : SAMPLE_NAMESPACE annotations : snapshot-tools.enigmo.co.jp/rotation : "true" snapshot-tools.enigmo.co.jp/rtime : 12345678 spec : source : persistentVolumeClaimName : SAMPLE_PVC volumeSnapshotClassName : SAMPLE_VSC snapshot-tools.enigmo.co.jp/rtime アノテーション は マニフェスト 生成時のタイムスタンプを表現している。 snapshot-tools.enigmo.co.jp/rotation アノテーション は世代管理の対象とするか否かのフラグになっている。 コードプッシュ コンテナアプリはVolumeSnapshot マニフェスト を作成、削除するとその変更をプッシュします。 プッシュ時のオプションでMergeRequest( github でいうPullRequest)を作成し、CIが成功したら自動でMRをマージするようにしています。 CIの実行 MRに対しては手動による マニフェスト のデプロイと同じ条件でCIが周り、問題がなければそれをマージするようになっています。 これにより、システムが生成した マニフェスト コードを特別扱いすることなく、統一したポリシー(例えば、フォーマッタやセキュリティチェック等の マニフェスト に対するバリデーション)を適用できます。 Argo CDによるデプロイ masterにマージされた変更を検出し、それを クラスタ へ自動で適用するようにauto-syncを有効化しています。 また、世代管理機能によってVolumeSnapshot マニフェスト の削除される場合もあるので、auto-pruneも有効化しています。 以下がArgoCD Applicationのサンプルになります。 apiVersion : argoproj.io/v1alpha1 kind : ApplicationSet metadata : name : snapshots spec : generators : - list : elements : - namespace : SAMPLE_NS1 - namespace : SAMPLE_NS2 - namespace : SAMPLE_NS3 template : metadata : name : '{{namespace}}-snapshot' spec : project : SAMPLE-PROJECT source : repoURL : GITLAB_URL targetRevision : master path : SAMPLE_PATH destination : server : K8S_ENDPOINT namespace : '{{namespace}}' syncPolicy : automated : prune : true 純粋なApplicationでなく、ApplicationSetとしている理由は我々は複数ネームスペースの運用をしているためです。 (Argo CDのApplicationは1つのネームスペースしかデプロイ先として指定できない。) まとめ Argo Workflowsを使ったPersistentVolumeの定期バックアップシステムについて紹介させていただきました。 実装方法としてオペレータパターンで開発する選択肢もありましが、cronの一時停止やカウントダウンタイマーなどを GUI で管理できる点からArgo WorkflowsのCronWorkflowを利用する実装を選択しました。 今回の記事が Kubernetes ネイティブな自動化システムの開発の参考になれば幸いです。 エニグモ ではエンジニアを含む各種ポジションで求人を募集しております。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
「安心して何度も利用したくなる マーケットプレイス 」を作る!UXデザイングループを紹介 エニグモ でTech職種の採用や、採用広報を担当している 廣島 です。 この記事は、 エニグモ で新入社員向けのオンボーディング研修として実施する 部署紹介プログラム の中で プロダクトマネージャーやディレクター、UI/UXデザイナーが所属するグループであるUXデザイングループマネージャーの山田さんがグループの説明をした内容をまとめた記事です。 グループのミッションや、チーム体制、カルチャー、どのように他チームと連携しながらプロジェクトを推進しているかについて説明します。最後に、 BUYMA サービスやUXデザイングループの今後の展望や、UXデザイングループで仕事を行う魅力についてもお話しします。 ※この記事は Enigmo Advent Calendar 2023 の25日目の記事です※ BUYMA サービスについて まずは、 エニグモ が運営するサービスの BUYMA について説明します。 BUYMA は累計会員数1000万人超えの海外ファッションNo.1の通販(EC)サイトです。 世界中に在住する約20万人のパーソナルショッパー(出品者)から、ファッションを中心とした世界中のアイテムを購入できる「売り手」と「買い手」が個人であるCtoCのグローバル マーケットプレイス です。 主に個人の海外在住の日本人のネットワークを構築して海外から商品を購入することが可能で、多種多様な購入者の要望に答えています。商品の約半分は海外から届きます。 BUYMA が間に入ることで安心安全に海外の買い物ができるプラットフォームとして運営しています。 出品アイテムの特徴としては高級品×希少品の商品のラインナップが充実しており、海外限定デザインや、国内未上陸ブランド、国内完売入手困難品など高付加価値のアイテムを世界中から購入することができます。 BUYMA はファッションを主力にしつつも、インテリア・アウトドア、旅行などカテゴリーを拡大しています。 BUYMA のビジネスモデルの詳細は下記をご覧ください。 https://enigmo.co.jp/business/ UXデザイングループのミッション・業務内容とは? エニグモ で ディレクション を担うグループはUXデザイングループ(以下UXDグループ)と呼びます。 (UXとは「ユーザーエクス ペリエ ンス」のことで、ユーザーが商品やサービスを通じて得られる体験を指します) UXDグループとは名前の通り、ユーザーのPain(痛み)に向き合いながら、サービスをより使いやすく・より良くしていくために解決すべきことを考え、実行するグループです。 上記により、ユーザー体験を向上させ、安心して何度も利用したくなるような マーケットプレイス へ BUYMA を成長させることがミッションです。 さらに、UXDグループは BUYMA サービスのプロダクト側(エンジニア・デザイナー)とビジネス側(MD、 マーケティング ・広告、データ、CS、オペレーション等)のちょうど中間に位置して両者を支えサービスを前進させる役割を担います。 具体的な業務領域は? 実際にどういった業務領域を担当しているかについてお話しします。 BUYMA の新機能・既存機能の改修・新サービスの企画・ ディレクション からUI/UXと幅広く担当します。一般的なWeb業界の言葉でいうと具体的には下記のような業務です。 プロダクトマネジメント プロジェクトマネジメント 制作 ディレクション / 進行管理 各施策・プロジェクトの効果検証 UX視点からの企画提案 / サービスデザイン 定性 / 定量 調査 UIデザイン フロントエンドプログラム(主にインタ ラク ションデザイン部分)の実装 上記からも分かるようにUXDグループは ディレクション を行うグループではあるが、UIデザインやフロントエンドプログラムなどの制作も行っていることが特徴的です。 フロントエンドプログラムは、Reactでの実装などはエンジニア部署のフロントエンドエンジニアが行いますが、インタ ラク ションデザインなど実際に画面上で動く CSS や JavaScript の実装はUXDグループがおこないます。 その為、企画から制作までをシームレスにグループ内で行えユーザーの反応や外部環境に合わせてスピーディーに柔軟に対応することが可能です。 色々言いましたがまとめると業務内容は下記となります。 BUYMA の様々な課題に向き合い最適なスコープでユーザーに価値を提供する ユーザーの反応を定性、 定量 の両側面から分析して企画・施策に繋げる スキルとスキル(企画と開発)の間に立ち仲介・翻訳を行い、場合によっては制作も行う グループの体制について Webサービス における様々なスキルを持ったメンバーが集まっています。特徴としては、ディレクター部門であるが、制作するメンバーもいることです。一般的な職種で言うと、プロダクトマネージャー、ディレクター、 SEO ディレクター、UI/UXデザイナー、フロントエンドエンジニアが所属します。 前項で説明した業務領域全てを1人のメンバーが網羅しているのではなく、それぞれのメンバーの得意分野を活かしながら多岐にわたる領域をカバーし業務を行っています。 さらにグループは下記2つのチームに分かれています。 ・ プロダクトマネジメント チーム : マーケティング戦略 系の施策を担当するチーム ・ UI/UXチーム :UX戦略系の施策を担当するチーム グループの体制を示した図です。 明確にチームが別れているのではなく、UXDグループのマネージャーがUI/UXチームのマネージャーを兼任していることもあり、定例は一緒に行っています。週1回グループの定例の他、プロジェクトベースで進めています。 実際の・ プロダクトマネジメント チームとUI/UIXチームの業務は下記のインタビュー記事をご覧いただくと、よりイメージできるかと思います。 ・ BUYMAのWebディレクター(PdM)紹介/エニグモの魅力は?UXDグループとは?インタビューしました! ・ UI/UXデザイナーインタビュー/ユーザーによりよい体験を届けプロダクト価値の向上に取り組む! 開発フロー・プロジェクトの中でのUXDグループの役割 プロジェクトの開発のフローの中で、UXDグループのメンバーが他職種メンバーと協力しながらどういった役割を担うのかについてお話しします。 プロジェクトや施策に伴う開発はエンジニアやビジネスサイドなど他チームメンバーと連携してプロジェクト型チームで進行します。 開発全体の流れは、UXDグループメンバー(ディレクター)が企画をまとめプロジェクトがスタートします。ディレクターとエンジニアのリードメンバーやビジネスサイドのメンバーで企画をどのようにシステムに落とし込むのかを要件定義します。その後、エンジニアのリードメンバーがシステム設計を担当し、タスクを分解してエンジニ アメンバー を アサイ ンします。 プロジェクトや施策はディレクターから発案されることもあれば、 マーケティング やカスタマーなど他の部門からの提案もあります。 要件定義はプロジェクトの特性により異なりますが、通常は主にディレクターが他の ステークホルダー との ヒアリ ングを通じて要求を洗い出し、システムをどう作るかをエンジニアと共に要件定義を進めるのが主流です。 プロジェクトの目標やKPIの策定もディレクターが中心に行います。各プロジェクトでは、新規機能を開発する際に、「なぜこの新機能を開発するのか」「新機能リリースにより、ユーザーにどのような体験や行動を期待するか」「新機能リリース時の成功の定義は何か」などを、各プロジェクトの ステークホルダー と明確にしています。 最後に サービスの課題や今後の展望、 エニグモ でUXDにジョインする魅力、どういった方がマッチするかについて、マネージャーの山田さんにインタビューしました。 サービスの課題や今後の展望 今期の重点テーマである「 BUYMA サービスをより安心できる体験にする」ことは引き続き進めていきます。安心と一言でいっても様々な側面が存在します。 たとえば、高級ブランドを扱っているため、ユーザーが商品が本物であるか不安に感じることもあります。これに対処するため、来期も偽物の不安を払拭するための施策を推進していきます。 また、ビジネスモデル上、高級品および希少品を扱っていることが強みですが、日用品と比べて購入頻度が低い傾向があります。そのため、サイトに訪れるタイミングが少なくサイトにユーザーが定着しにくく、サービスとユーザーの距離が遠くなることが課題です。 この課題に焦点を当て、ユーザーがサイトに定期的に訪れる動機づけや、サービスとの継続的な関係を築くための取り組みにも注力していきたいと考えています。 UXデザイングループの魅力 プロジェクトごとに各職種のメンバーが アサイ ンされ、小規模チームが結成されます。そのためメンバー、一人ひとりが大きな裁量を持ち、エンジニア、デザイナー、ビジネスメンバーと密に連携し、距離が近く風通しも良く別部門という感じはしません。良い人(互いを尊重し、前向きなメンバー)が多いため、仕事上の人間関係による変なストレスがあまりなく、プロダクト開発方法や体制を共に進化させるマインドが根付いています。 また、グローバル×CtoCサービスという、 BUYMA 独自の仕組みやUXを構築するフェーズに携わることができます。出品者側と購入者側の双方のデータやユーザ―行動に基づくデータドリブンな開発が可能です。プロジェクトの成果を数値(売上、CVRなど)やユーザーの声によって直接実感することができます。 どういった方がマッチするか 経験やスキルも大事ですが、熱意やマインド面、カルチャーへの共感、 エニグモ へのバリューへのマッチを大切にしています。※バリューについての詳細は こちら 業務領域が幅広いため、俯瞰して物事を柔軟にアプローチできる方や、自らイニシアチブを取り推進できる方が活躍できるかと思います。 決まった案件をこなすだけでなく若手から裁量を持って働け、新規機能や新規事業の企画を自ら発案しユーザーに届け、世の中を変える可能性があります。 課題を見つけて積極的にア イデア を提案し進めていく方がマッチすると思います! BUYMA や関連サービスのサービス品質を常に進化させ、国内および世界各国のユーザーがますますサービスを利用して喜びや満足を得られるような機能を、楽しみながら共に考え抜いていただける方にジョインいただけると嬉しいです。
アバター
こんにちは、 エニグモ 嘉松です。 BUYMA のプロモーションや マーケティング を行っている事業部に所属、その中のデータ活用推進室という部署で会社のデータ活用の推進や マーケティング ・オートメーションツール(MAツール)を活用した販促支援、 CRM などを担当しています。 この記事は Enigmo Advent Calendar 2023 の 25 日目の記事です。 はじめに この記事では Google から提供されているBigQueryのオンライマニュアル「関数のベストプ ラク ティス(Best practices for functions)」を試してみた結果を紹介していきます。 「関数のベストプ ラク ティス」(日本語版) https://cloud.google.com/bigquery/docs/best-practices-performance-functions?hl=ja 「関数のベストプ ラク ティス」では、以下の4つのベストプ ラク ティスが紹介されています。 文字列の比較を最適化する 集計関数を最適化する 分位関数を最適化する UDF を最適化する はじめは4つ全てを1つの記事で紹介するつもりでしたが、記事を制作していく中で1つでもそれなりの記事ボリュームがあることが分かったので、読みやすさを重視して1つの記事で1つのベストプ ラク ティスを紹介していくことにしました。 ということで、この記事は「その1」として「文字列の比較を最適化する」を試した結果を紹介していきます。 文字列の比較を最適化する ベストプ ラク ティス 可能であれば、 REGEXP_CONTAINS ではなく LIKE を使用します。 BigQuery では、 REGEXP _CONTAINS 関数または LIKE 演算子 を使用して文字列を比較できます。 REGEXP _CONTAINS は多くの機能を提供しますが、実行に時間がかかります。 REGEXP _CONTAINS ではなく LIKE を使用すると、処理時間が短くなります。 特に、 ワイルドカード 一致など、 REGEXP _CONTAINS が提供する 正規表現 をフルに活用する必要がない場合には処理時間が短くなります。 次の REGEXP _CONTAINS 関数の使用を検討してください。 SELECT dim1 FROM `dataset.table1` WHERE REGEXP_CONTAINS(dim1, '.*test.*'); このクエリは次のように最適化できます。 SELECT dim1 FROM `dataset.table` WHERE dim1 LIKE '%test%'; なるほど。やはり機能の多い関数、複雑なパターンに対応できる関数よりも、単純なことしか出来ない関数の方が処理は軽い(速い)と。言われてみれば当たり前といえば当たり前ではありますが。単純なことしてできない関数が利用できるケースであれば、そちらを使ったほうが良い、 正規表現 による検索が必要ない場合は LIKE を使いましょう、ということですね。そもそも REGEXP_CONTAINS より LIKE を使うことを最初に考えるとは思うけどww( REGEXP_CONTAINS の方が汎用性が高いので常に REGEXP_CONTAINS を使っています、みたいな人は注意が必要です!) 試してみた 検証方法 REGEXP_CONTAINS と LIKE を使ったクエリを実行して比較していきます。 比較する候補となる値は 経過時間 と 消費したスロット時間 の2つ、クエリの「実行の詳細」から取得できます。 それぞれの意味は以下の通りです。 経過時間 クエリが開始されてから完了するまでの時間 サーバの使用状況などによる待機時間も含まれます 消費したスロット時間 「スロットとは、 SQL クエリの実行に必要な演算能力の単位です。」(「BigQueryのコンソールの(?)」より) スロットについての詳細は以下の Google のマニュアル「スロットについて」を参照ください。 https://cloud.google.com/bigquery/docs/slots?hl=ja BigQueryスロットは、BigQueryでSQLクエリを実行するために使用される仮想CPU ということで、ざっくり言うとクエリ( SQL の実行)に使用したCPU使用量です。 今回の検証では 消費したスロット時間 を比較ます。( 経過時間 だと Google のその時のサーバの負荷といった外部要因が加わるため) クエリはそれぞれ5回ずつ実行してその平均値を比較します。 クエリを実行するとクエリの結果がキャッシュされるので、キャッシュを使用しないように設定を行います。 なお、対象のテーブルは約1億件のテーブルを対象にしました。 キャッシュクリアの方法 検証中はキャッシュを使用してしまうと正しい計測ができないので、キャッシュを使用しないように設定を行います。 https://cloud.google.com/bigquery/docs/best-practices-performance-functions?hl=ja#optimize_string_comparison 検証結果 消費したスロット時間(時間:分:秒) 試行回数 REGEXP _CONTAINS LIKE 1 0:03:06 0:02:50 2 0:03:22 0:02:29 3 0:03:02 0:02:36 4 0:03:06 0:02:20 5 0:02:56 0:02:18 平均 0:03:06 0:02:31 REGEXP_CONTAINS を使用した場合の平均の「消費したスロット時間」は 3分6秒 だったのに対して、 LIKE では 2分31秒 と35秒短縮、減少率は 80.79% と約80%に短縮、約20%改善されました。 この20%の改善をどう取るか。かなり短縮できたと取るか、さほど変わらないと取るか。 個人的には、思ったほど変わらないな、という印象でした。 といのも、 SQL ではテーブルの結合方法(JOIN)や、絞り込み条件(WHERE句)をチューニングすると実行時間が半分になったり、場合によっては1/10になったりすることもざら、珍しくないため。 ただ、単純に LIKE を使うだけで20%削減されるのであれば、それは価値ありますよね。 このクエリで検索の対象としているカラムの平均の文字数は33文字と少なかったため大きな差が出なかった、もう少し文字数が多いカラムを対象にしたらもう少し差が出るのでは、と考え文字数の多いカラムを使って検証を実施しました。 追加検証結果(時間:分:秒) 平均文字数が758文字のカラムを使って比較 消費したスロット時間(時間:分:秒) 試行回数 REGEXP _CONTAINS LIKE 1 0:59:53 0:55:26 2 1:02:00 0:53:43 3 1:02:00 0:51:51 4 1:01:00 0:50:43 5 1:04:00 0:52:02 平均 1:01:47 0:52:45 まずカラムの平均文字数が増えたことで「消費したスロット時間」も増加しています。 平均文字数が33文字では約3分だったのに対して、758文字は約60分と約20倍に。 当然ですが検索する文字数が増えればその分として処理の時間も増えますね。 REGEXP_CONTAINS を使用した場合の平均の「消費したスロット時間」は 1時間1分47秒 だったのに対して、 LIKE では 52分45秒 と9分2秒短縮、減少率は 85.39% と約85%に短縮、15%改善されました。 平均文字数が33文字のときは約80%に短縮されたのに対して、758文字では約85%と短縮の率は減少しました。 この辺りの差は文字数のバラツキや検索する文字によっても差が出るのかもしれません。 文字数の多いカラムでは LIKE を使うことでよりパフォーマンスに差が出ると思いましたが、大きな違いはありませんでした。 まとめ REGEXP_CONTAINS と LIKE を使ったクエリを実行して比較してみた結果、LIKEを使った方がクエリのパフォーマンスは確かに改善された! その改善率は今回の検証では約20%だった!! 可能であれば、 REGEXP_CONTAINS ではなく LIKE を使用しましょう!!! 本日の記事は以上になります。 エニグモ Advent Calendar 2023 もこの記事で最後になります。 最後まで読んでいただきありがとうございました。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは! 今年7月に中途入社しました、コーポレートエンジニア(コーポレートIT[CO-IT]チーム) のフルセです! 今年も終盤(早いですねぇ、、)ということで、 Enigmo Advent Calendar 2023 の季節になりました!! クリスマスイブである 24 日目 を担当する私は入社エントリ・振り返りなど中心に自由に書きたいと思います! なお、この記事が少しでもコーポレートエンジニアに興味がある方や入社を検討してくださっている方の参考になれば幸いです。 Embed from Getty Images window.gie=window.gie||function(c){(gie.q=gie.q||[]).push(c)};gie(function(){gie.widgets.load({id:'eGD2T_vEQIpzxevVRkRvfQ',sig:'E4aHXJwgZtXDaAqpL9J550lu8QN5ONksi1IwL5kG1WY=',w:'556px',h:'311px',items:'531047383',caption: true ,tld:'com',is360: false })}); 簡単な経歴紹介 そもそもコーポレートエンジニアって何? 実際に入社してみて感じたこと 入社してからの仕事とその感想 内容 感想 こんな方に向いているかも(個人見解) 最後に 簡単な経歴紹介 私の経歴についてざっくり下記のようにまとめました。 ① 新卒でシステムインテグレータ企業にSEとして入社 →ITの基礎知識・技術を獲得! and IT業界の厳しさを痛感、、(大袈裟) ② システムインテグレータ企業を退職 →主にシステム相手の業務だったので、より人と接する仕事がしたいと考え決意 ③ 某アパレル商社の情シスへ転職 →培った知識とスキルを活かし新たな業種へチャレンジしたかった! →情シスが意外と性に合っていることに気づくきっかけに! ④ カナダへ留学 →ずっと挑戦したかった海外留学に挑戦! →語学はもちろん人との出会いや経験から人生にプラスになった! ⑤ エニグモ にコーポレートエンジニアとして入社 →帰国後にご縁があり、経験を活かしコーポレートエンジニアとして入社! そもそもコーポレートエンジニアって何? コーポレートエンジニアとは? コーポレートエンジニアとは、企業内のIT活用や運用を担当するエンジニアのことです。 企業のIT環境の変化は著しく、テレワークの普及などによって クラウド サービスの利用も活発になり、 SaaS 型のID管理や統合認証サービスを利用する企業が増えてきました。そのなかでコーポレートエンジニアは、社内ITの構築・運用をはじめとして、社内業務の課題解決のための企画立案や部門間の調整まで幅広い業務を担います。 もっと詳しく知りたい方は、チームメンバーの記事を読んでみてください! tech.enigmo.co.jp 実際に入社してみて感じたこと コーポレートエンジニアとして働く以上、社員の方々とのコミュニケーションをかなり重要だと考えていました *1 。 しかし、 エニグモ はリモートワーク中心の会社のため、Face to Face でのお話しする機会が少なくどのように交流の輪を広げていこうか少し不安(そもそも入社直後というのもあり。。)に感じていました。 そんなときに、2M(Monthly Meet-up) *2 に参加させていただきました。 そこでは色々な部署や役職の方と分け隔てなくフランクにお話しすることができ、一瞬にして不安が和らぎました。 やはり、新しい環境でのスタートはどうしても孤独感や不安がつきものだと思いますが、こういった交流会があると気持ちよくスタート ダッシュ が切れますよね!そもそも、こういった交流会を定期的に行なっている企業というのは少ないと思うので、これぞ エニグモ の良いところだと思っております。 それからというもの、毎月可能なかぎり参加し交流の輪を広げております!(無料で美味しいお酒とご飯が得られることも理由の一つなのですが笑) 2Mについて気になる方は、レポート記事がありますので是非ご覧いただければと! www.wantedly.com 入社してからの仕事とその感想 入社してからのざっくりとした仕事内容とその感想を書かせていただきました! 内容 社内ヘルプデスク対応 内容:PCの故障や不具合、各 種IT サービスのトラブル対応等 対応頻度:1〜2件/日 アカウント管理 内容:アカウントの付与・削除から数量管理・購入 対応頻度:1〜2件/週 IT機器の管理・キッティング 内容:PCやモバイルデ バイス の発注・管理・キッティング 対応頻度:10〜20件/月 社内ナレッジの作成・整備 内容:手順書やルールの新規作成・ブラッシュアップ 対応頻度:2回/週 中途入社者向けのオリエン 内容:PCのセットアップ方法の説明、社内ITサービスルールの共有 対応頻度:1〜2回/月 社内不要OA・IT機器廃棄対応 内容:廃棄物回収対応 対応頻度:2回/年 カオスマップ作成 内容:社内で利用中サービスを整理と今後導入が必要なサービスの洗い出し 対応頻度(見直し):2回/年 感想 一覧にしてみると、今年は色々なことをやらせていただいたと改めて感じています。 裁量を任せていただいているため、日々に マルチタスク をこなしておりますが、 それもまた自分のタスク管理力や遂行力の向上につながっていると感じます。 個人的にやりがいがあった業務としては、「社内不要OA・IT機器廃棄対応」です。 業者選定から始まり、やりとり(契約締結や回収日の調整など)、社内ナレッジ作成、各方面との連携など、これぞコーポレートエンジニアと感じるようなタスクだったため、とてもやりがいがありました。 それと山積みだった廃棄端末類をスッキリさせることができたという達成感が大きかったですね笑 またタスクとして面白かったものとしては、「カオスマップ作成」です。 初めてカオスマップというものを作成したので、そもそもカオスマップとは?からスタートし、他企業のカオスマップの調査、社内ITサービスの整理、今後導入が必要なITサービスの洗い出しを行いました。その後は、カオスマップをパズルのように作成する大変な作業がスタートし...と、ここでは書ききれないので、詳しい作成プロセスなどは機会があればまた書かせていただきたいと思います。(今回は割愛させていただきます。) それからというもの、試行錯誤を繰り返し、レビューを重ねてやっとの思いで完成させることができました。 結果として、時間をかけて取り組んだからこそ、サービスに対する理解や知見を広くできましたし、社内の課題(足りていないサービス)も炙り出すことができました。とはいえ、まだまだ未熟なカオスマップだと思っているので、これからもっと成熟させていきたいと思います。(楽しみ!) こんな方に向いているかも(個人見解) コーポレートエンジニアとしてのキャリアをスタートしたばかりの私ですが、 そんな私だからこそ考えるコーポレートエンジニアにはこんな人が向いているのではないかというのをまとめてみました! 私が思うに下記の3つの要素が肝になってくると思います。 コーポレートエンジニアの必要要素 新しい情報・技術に興味があり探求するのが好きな方(好奇心) 日々進化する IT技術 やサービスをいち早く自社へ展開・導入するため 最新の情報に敏感であれば、サービスなどがアップデートされたとしても柔軟に対応することができるため 人とコミュニケーションするのが得意・好きな方(コミュニケーション力) 業務上(問い合わせ対応・情報共有など)何かとコミュニケーションが必要になるため 問題を切り分けて解決することが得意・好きな方(柔軟性) コーポレートエンジニアは企業の多くの課題を与えられるので、一つ一つ解決し柔軟に対応することが求められるため ここで書かせていただいた3つの要素は、かなり重要なポイントになると考えます。 コーポレートエンジニアの業務は、IT関連の何でも屋のようになりがちだと思っております。 そのためタスクを挙げ出したらキリがないうえ、日々変化します。 そのため、柔軟性はもちろん、社内外連携のためのコミュニケーション力やITサービスに関する知見を広げるための好奇心が重要になってきます。 もちろん、足りてないと感じている要素があったとしても、業務を通じてレベルアップすることは可能だと思います!(私も絶賛レベルアップ中...) ※あくまで個人の見解なのでご了承ください。 最後に 私もまだまだ未熟なので、日々進化する IT技術 ・情報に置いていかれないよう自己研鑽を続けつつ、 社内の課題を一つずつ着実に解決し、より良い社内環境づくりに励みたいと思います! 最後まで読んでいただき、ありがとうございました!! 株式会社 エニグモ すべての求人一覧 hrmos.co *1 : 情シス時代に業務上のコミュニケーションがかなり重要だと理解していたため *2 : 毎月月初に開催される社内交流会
アバター
こんにちは、インフラグループ Kubernetes チームの福田です。 この記事は Enigmo Advent Calendar 2023 の22日目の記事です。 皆さんは Kubernetes のアップグレード、どうしていますか? Kubernetes は4ヶ月に一回、新しいマイナーバージョンがリリースされ、最新の3つのマイナーバージョンのみサポートされます。 つまり、原則は4ヶ月に一度、アップグレードをやらなければなりません。(最新バージョンであれば最大12ヶ月はサボれるという考え方もありますが。。。) 弊社では Kubernetes 環境としてEKSを使っており、 Kubernetes 本体のリリースと微妙に間隔が違いますが、最新を維持するために大体3〜5ヶ月毎にアップグレード作業をやっています。 この Kubernetes (EKS)のアップグレード作業をワークフロー化したのでそれを紹介します。 ワークフローとは 本記事でいうワークフローとはワークフローエンジンを使って自動化された作業のまとまりです。 ワークフローエンジンとは、作業プロセスをいい感じに管理してくれるツールです。 代表的なものとして、 Apache Airflow , digdag , Argo Workflows などがあります。 我々の場合、 Argo Workflows を利用しています。 EKSアップグレードワークフローの概要 上図は実際のワークフローのキャプチャです。 3段階の構造(上から順に実施される)になっていて、以下を実施する内容となっています。 廃止される API を使っていないかチェック EKSアドオンのアップグレードバージョンを特定 CloudFormationテンプレートを更新するPullRequestを出す。 元々、弊社では AWS リソースをCloudFormationで管理する運用になっていたことから、ワークフローのゴールがCloudFormationコードの修正(Pull Request作成)となっています。 ワークフローの入力 上図の通り、ワークフローには入力パラメータがあります。 ticket-id JIRAのチケットIDを指定します。 コード修正時のコミットメッセージやブランチ名の プレフィックス として利用されます。 kubernetes -cluster アップグレードを実施する対象の クラスタ をプルダウン形式で選択します。 eks-version Kubernetes versionをマイナーバージョンまで指定します。例: 1.28 ami EKSノードで使用するAMIを指定します。 EKSアップグレードワークフローの詳細 1. 廃止される API が無いかチェック Kubernetes ではマイナーバージョンの更新に伴って、機能追加や古い機能の削除のために API Versionが更新され、既存のものが廃止されることがあります。 そのため、 Kubernetes アップグレードを実施する前に廃止される API が無いかチェックしています。 pluto という OSS を使って、 クラスタ 上にあるリソースで廃止されるものを使っていないかチェックをしています。 2. EKSアドオンのアップグレードバージョンを特定 EKSでは Kubernetes のマイナーバージョンの更新と併せて、EKSアドオンのアップグレードが必要な場合があります。我々の場合、「EKSアドオンのバージョンはデフォルトバージョン※を指定する」というポリシーで運用しています。 ※ EKSではアドオン毎に Kubernetes のマイナーバージョンに紐づくデフォルトのバージョンを公開しています。 具体的な方法としては aws コマンドで、指定した Kubernetes のマイナーバージョンに対応するEKSアドオンのデフォルトバージョンを取得しています。 aws eks describe-addon-versions --addon-name ADDON_NAME --kubernetes-version K8S_NAME --query 'addons[0].addonVersions[?compatibilities[0].defaultVersion==`true`].addonVersion | [0]' 3. CloudFormationのコードを管理している リポジトリ に Kubernetes アップグレードのためのPull Requestを出す このステップについては前のステップで得られるEKSアドオンのアップグレードバージョンとワークフローへの入力値を元に、CloudFormationのコードを修正して、Pull Requestを出します。 また、Pull Requestに人手で実施する作業内容を記載します。 このステップにおける処理はGoで実装した自前の CLI ツールで行っています。 考慮事項 このステップを実装するにあたり、考慮したことを紹介します。 VPC CNIのアップグレード 前のステップでEKSアドオンのアップグレードバージョンを特定していますが、 VPC CNIについてここで問題があります。 VPC CNIのアップグレードは一度に1つのマイナーバージョンのみアップグレード可能です。 例えば、 Kubernetes 1.27に対応する VPC CNIのデフォルトバージョンが v1.12.6-eksbuild.2 で Kubernetes 1.28に対応する VPC CNIのデフォルトバージョンが v1.14.1-eksbuild.1 の場合、予め VPC CNIのバージョンをv1.13系にアップグレードする必要があります。 実際の実装としては VPC CNIのアップグレードバージョンのマイナーバージョンが元のバージョンより2つ以上離れている場合は、本ワークフローを実施する前に VPC CNIのマイナーバージョンをアップグレードするように促すメッセージを出してワークフローはそこで失敗するようにしています。 手順書 Kubernetes アップグレードの実施自体の手順としてはCloudFormationの実行コマンドだけですが、実際の作業手順はもう少し複雑です。 我々の場合はアップグレードの実施コマンドに加えて以下を盛り込む必要がありました。 作業前 作業通知の開始アナウンス 作業時の監視アラートのサイレント設定 作業後 アップグレードが正しく完了したかの確認 作業時の監視アラートのサイレント設定解除 作業通知の終了アナウンス yaml をGoで編集する際の副作用 CloudFormationのテンプレートは yaml ですが、これをGoで書き換えると発生する副作用があります。 それはGoで yaml を扱うために yaml ファイルを エンコード して、プログラム上で何らかの処理をして、再度 yaml ファイルに戻してやると空行やコメント等が消失してしまうというものです。 yaml としては等価なのだからという理由で切り捨ててしまえばそれまでですが、コメントや空行はリーダビリティの観点で有用なものも多いので、これは維持するようにしたいです。 まず、コメントの維持については yaml.v3 を使うことで解決できます。 具体的には以下のように yaml 中にある書き換えたいプロパティをpathで指定して、それをnewValueで更新するようにしています。 func (e *yamlEditor) updateYamlProperty(node *yaml.Node, path [] string , newValue string ) error { if len (path) == 0 { return fmt.Errorf( "invalid path" ) } for i := 0 ; i < len (node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+ 1 ] if keyNode.Value == path[ 0 ] { if len (path) == 1 { valueNode.Value = newValue return nil } else if valueNode.Kind == yaml.MappingNode { return e.updateYamlProperty(valueNode, path[ 1 :], newValue) } } } return fmt.Errorf( "invalid path" ) } 空行についてはdiffコマンドのオプションで空行を無視する差分のdiffを生成して、それをpatchコマンドで元のファイルに修正として適用するようにしています。 ## 元々存在するcloudformation template target.yaml ## プログラムによって書き換えが行われたcloudformation template。空行はなくなってしまっている。 target.changed.yaml diff -U 0 -w -b -B target.yaml target.changed.yaml > target.diff patch -i target.diff target.yaml 参考: https://github.com/mikefarah/yq/issues/515#issuecomment-830380295 まとめ 今回はArgo Workflowsを使った Kubernetes (EKS)のアップグレードについて紹介させていただきました。 紹介したワークフローはKubernetes1.27とKubernetes1.28のリリースの間で作成したもので、Kubernetes1.28のアップグレードはこのワークフローを使ってアップグレードしました。 これのおかげで面倒だったアップグレード作業を大分楽にすることができました。 明日の記事の担当は早野さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは。エンジニアの竹田です。 BUYMA の検索システムやMLOps基盤の開発・運用を担当しております。 こちらは Enigmo Advent Calendar 2023 の21日目の記事です 🎄 弊社では2021年頃よりMLOps基盤を Google Cloud PlatformのAI Platform Pipelines上に構築して開発・運用を行っています。 この度、Vertex AI Pipelinesへの移行を全面的に進めることになりましたので、ご紹介も兼ねて記事にしたいと思います。 背景 2024/07にAI Platform Pipelinesが非推奨になるという通知を受けたことがきっかけです。 AI Platform Pipelines deprecations  |  Google Cloud 非推奨の通知が移行を開始するトリガーではあったものの、かねてからGKE クラスタ の運用をどうにかできないかなと考えていました。 Vertex AI Pipelinesへの移行によりフルマネージド化できるのは大きなモチベーションとなっています。 移行における課題 この機会にKubeflow Pipelines SDK (以下、kfp) v1からv2へのアップグレードを進めています。 移行ドキュメント も提供されているため、それほど苦労はしないものと考えておりました。 が、結果としてこの選択が多くの苦労を抱えることになってしまいました 😢 実際に kfp v2 利用してみて、良かった点・苦労している点を交えてご紹介いたします。 ※下位互換で動作させることも可能でしたが、kfp v1での機能拡張は行われない、そのうち書き換えが必要になる、という点を考慮してkfp v2への書き換えに踏み切りました。 kfp v1からv2へ kfpは、 kubernetes 上で 機械学習 パイプラインを動作させるためのツールキットです。 コードベースは Python です。 An introduction to Kubeflow パイプラインを作成してVertex AI Pipelines上で動作させると、動作フローが視覚的に分かりやすく表現されます。 Python で記述したコードを コンパイル して利用する性質上、kfp v1の頃からかなりクセが強いなとは感じていました。 実際に利用してみた上での良い点、苦労している点を列挙し、所感を書いていこうと思います。 kfp v2の利用バージョン # pip list | grep kfp kfp 2.3.0 kfp-pipeline-spec 0.2.2 kfp-server-api 2.0.3 kfp v2の良い点 入出力に利用する Input[xxx] / Output[xxx] が便利 ParallelFor による並列処理の結果を Collected で受け取れるようになった @dsl.component や set_accelerator_type が直感的 入出力に利用する Input[xxx] / Output[xxx] が便利 kfp v2では入出力に利用するオブジェクトがこの形(基本的に Input[Artifact] / Output[Artifact] の利用)にほぼ統一されており、GCS上のパスを意識せず利用できるため非常に便利です。 https://www.kubeflow.org/docs/components/pipelines/v2/data-types/artifacts/ ParallelFor による並列処理の結果を Collected で受け取れるようになった kfp v1でも ParallelFor は利用できましたが、fan-in(複数の入力を一つにまとめること)が厄介でした。 kfp v2では最近になって Collected が利用可能となり、 ParallelFor の後に呼び出すことで結果をリスト形式でfan-inできるため、コードの可読性も飛躍的に向上します。 @dsl.component や set_accelerator_type が直感的 個人的な好みの部類かもしれませんが、kfp v2は定義周りが直感的になった印象があります。 https://www.kubeflow.org/docs/components/pipelines/v2/migration/#create_component_from_func-and-func_to_container_op-support kfp v2で苦労している点 変数展開がおかしくなることがある 型指定の厳密化により、何を渡せばよいのか分からなくなることがある 変数展開がおかしくなることがある kfp v2への移行で最も困っている点です。以下のようなissueも挙がっています。 https://github.com/kubeflow/pipelines/issues/10261 変数内に何らかの文字列や数値を入れているはずが、実際に利用する場合に以下のような展開がされてしまいます。 {{channel:task=;name=g;type=String;}} コンポーネント の出力結果をうまく展開できない場合は以下のような内容です。 {{channel:task=term-calc;name=list_date;type=typing.List[str];}} 機械学習 の初回実行プロセスの多くがBigQueryからのデータ取得を行っており、データ取得期間や特徴量を変数で管理しているため、既存のパイプラインコードでは Python のformatメソッドによる書式変換を多用しています。 この書式変換のほとんどが正常に動かなくなってしまい、試行錯誤を繰り返すことになってしまいました。 以下、期待した変数展開とならずにエラーとなるパターンの一部です。 パイプライン引数や コンポーネント の返却結果をdictに加えて コンポーネント に渡した場合 @ dsl.component def convert_str (tmpl: str , value: dict , output: Output[Artifact]): with open (output.path, "w" ) as f: f.write(tmpl.format(value)) @ dsl.pipeline (name= "test" , description= "test prediction" ) def test_pipeline (table_name: str = "sample" ): value = { "table_name" : table_name } sql = "SELECT * FROM {0[table_name]}" convert_str_op = convert_str(tmpl=sql, value=value) コンパイル 時のエラー内容 ValueError: Value must be one of the following types: str, int, float, bool, dict, and list. Got: "{{channel:task=;name=table_name;type=String;}}" of type "<class 'kfp.dsl.pipeline_channel.PipelineParameterChannel'>". パイプライン引数をパイプライン本体で利用しようとした場合 @ dsl.pipeline (name= "test" , description= "test prediction" ) def test_pipeline (periods: str = "1m" ): target_file = f "periods_{periods}.yaml" with open (target_file, mode= "r" ) as f: periods_conf = yaml.safe_load(f) コンパイル 時のエラー内容 FileNotFoundError: [Errno 2] No such file or directory: 'periods_{{channel:task=;name=periods;type=String;}}.yaml' 型指定の厳密化により、何を渡せばよいのか分からなくなることがある パイプライン引数を利用しようとして以下のようなエラーが出たり TypeError: PipelineParameterChannel is not a parameter type. 特定 コンポーネント にinputを渡した場合に以下のようなエラーが出たり ValueError: Constant argument inputs must be one of type ['String', 'Integer', 'Float', 'Boolean', 'List', 'Dict'] Got: <kfp.dsl.pipeline_task.PipelineTask object at 0x7f8a03f89880> of type <class 'kfp.dsl.pipeline_task.PipelineTask'>. といったことが割と発生します。 自分としては正しい型での引き渡し、および参照をしているつもりのため、どう対処してよいか分からなくなることが多いです。 事前にキャストすることで正常に動作することもあれば、そもそもデータ型の扱いを見直す必要があったりします。 所感 上述の苦労している点での引っ掛かりが多いのが難点で、残念ながら使いやすさは感じられていません。 ですが、一度形としてできてしまえばテンプレート化できると思われるため試行錯誤しながら進めている、というのが現状です。 コンテナイメージ化している コンポーネント 内部の挙動はほぼ変更なしで動作しており、ほとんどが「変数展開がおかしくなる」部分の障壁により思うように進捗していないといった状態です。 具体的にこうすれば良い、といったアプローチが見つけられたら何かしらの形で記事にできればと考えております。 引き続き、Vertex AI Pipelines移行による 機械学習 基盤のフルマネージド化を目指して邁進していく所存です 💪 おわりに 明日の記事の担当はインフラチームの福田さんです。EKS周りのお話です。お楽しみに!! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、サービスエンジニアリング本部の平井です。 こちらは Enigmo Advent Calendar 2023 の 20日 目の記事です。 私は、エンジニア部門で取り組んでいる開発生産性分析について紹介します。 開発生産性分析を試みた経緯 現在、 エニグモ では開発組織体制の変更、メンバー増強など様々な組織強化を目指した動きが加速してきています。ただ、そのような施策が開発組織のパフォーマンスを向上させているのか 定量 的な指標で測ることができませんでした。 また、開発組織としては、開発を通して一定のスピードでユーザーに十分な価値を届ける責任を負っているものの、開発スピードを測る良い方法がありませんでした。 このように組織パフォーマンス向上施策の結果を確認するため、開発組織としての開発スピードを図るために開発生産性分析の取り組みが始まりました。 また、この取り組みは開発生産性に興味がある有志のメンバーが集まり進めていきました。 指標選定 分析する指標としては、 Google が提案している Four Keys を参考にしました。 Four Keysを参考にした理由は以下になります。 開発スピードを測る指標が含まれている。 自分達で計測、分析の基盤を整えられそう。 一般的な指標である。 そして、 指標の中で速度に関わる指標であり、計測が容易そうな 変更のリードタイム と デプロイ頻度 を計測することが決まりました。 デプロイ頻度に関しては、営業日や開発メンバーの増減による影響を少なくするために、 @hiroki_daichiさんが紹介されていた d/d/d というやり方で 1日あたりの1開発者あたりのデプロイ回数を計算することにしました。 また、現在 BUYMA は基幹システムと複数のマイクロサービスで構成されていますが、どの開発チームも修正することが多い基幹システムにおけるこれらの指標を計測していくことになりました。 各指標の定義 前提として、 BUYMA の基幹システムはGitlabを ホスティング サービスとして利用しています。 本番環境へのデプロイは内製アプリケーションを通して行われ、開発者が各々デプロイ依頼を作成します。 本番環境へのデプロイプロセスは1日3回実行されるため、各開発者は作成した依頼をそのどれかのプロセスに乗せて本番化します。 Four Keysを参考にしつつ、このような BUYMA の特徴を考慮して各指標の定義をチームで相談して決めました。 その上でリードタイムは以下の定義で計算しています。 MR内の最初のコミットからそのMRが本番環境に反映されるまでの時間 各開発者によって開発手法は異なるため最初のコミットタイミングが微妙にずれる可能性はありますが、 開発 -> レビュー -> QA -> デプロイ という一連の 開発プロセス を計測できるためこのような定義にしました。 d/d/dに関しては、 デプロイ回数/営業日/開発者数 となっていて、それぞれ以下のような定義になっています。 デプロイ回数: 本番化された本番化依頼の数 BUYMA の本番化の特徴として、1デプロイに複数の修正が含まれるためその一つ一つの修正を個別に数えたかったためこのような定義にしました。 営業日: 土日祝日を抜いた平日 営業日は特に一般的に使われているものです。 開発者数: マージされたMRに参加したユニークGitLabユーザー数 エンジニアリング部門に属していても「基幹システムに関わっていない人は含めたくない」、「過去の特定期間の開発者数も出したい」など考慮する点が多く難しかったのですが、「MRに関わった人数は開発に関わる人数と等しいだろう」という考えのもとチームで相談してこのような定義になりました。 データ収集、可視化の仕組み 次に、指標の計測と可視化の仕組みについて説明します。 Airflowにワークフローを構築して、MRに関わる情報をGitLabの API から収集しBigQueryに格納しています。 デプロイに関する情報は内製本番化アプリケーションが利用しているデータベースに永続化されているため、BigQueryに連携してGitLabの情報とJOINできるようにしました。 可視化に関しては、BigQuery上のレコードを SQL で加工してLookerの ダッシュ ボードを使うことで実現しています。 開発性生産性分析システム構成図 リードタイムに必要なデータ収集 リードタイムを計測するために以下のGitLab API を利用しています。 基本的には API のレスポンスから日次の差分データを取得して、そのままBigQueryのテーブルに保存しています。 List all projects API GitLabのプロジェクトのマスターデータを収集するために利用しています。 収集したデータはLookerで可視化する際にプロジェクト名を表示するなどに利用しています。 List group merge requests API MRデータを収集するために利用しています。 マージ済みMRの情報のみ必要なので API パラメータを使ってマージ済みMRの情報のみ収集しています。 Get single merge request commits API MRに紐づくコミットの情報を収集するために利用しています。 ここで取得したデータを利用してMRに紐づく最初のコミットを特定して、リードタイムを計算します。 d/d/d に必要なデータ収集 リードタイムを計測するために以下のGitLab API を利用しています。 こちらも日次の差分データをそのままBigQueryのテーブルに保存しています。 List a Project’s visible events API GitLabのイベント情報を収集するために利用しています。 マージされたMRに参加したユニークGitLabユーザー数を計算する際にここで収集したデータを利用しています。 可視化について 先述したように可視化にはLookerの ダッシュ ボードを利用しています。 LookML で SQL を組み立てて指標を計算しています。 エニグモ では BUYMA をメインの機能で分割して開発チームを組織していて、それらを ドメインチーム と呼んでいます。指標の監視や分析は各 ドメイン チームにやってもらっているため各 ドメイン チーム毎に ダッシュ ボードを作成しています。 可視化したあるチームのリードタイム 運用について 指標の監視や分析は、細かいやり方を指定せずに ドメイン チームに依頼しています。 例えば私が所属しているチームでは毎週の振り返り時にリードタイムとd/d/dを確認して、何か問題があれば改善案を考えるという運用をしています。 また、開発生産性指標の計測にあたり、 開発者にMRへの ラベル 付与を依頼しました。MRに付与されたラベル情報をもとに ドメイン チーム毎のリードタイムを計測しています。 今後の課題 今後の課題としては以下になります。 BUYMA 基幹システム以外でまだデータ収集できていないマイクロサービスがあるため正確に開発組織全体の生産性を測れていない。 要件定義、仕様決定などの ディスカバリー フェーズにかかっている時間を図れていない。 変更障害率、サービス復元時間など安定性の指標を図れてない。 現状だとリードタイムやd/d/dが向上した際に、それが障害発生による細かい修正が増えたのが原因なのかわからない。 終わりに 今回はエンジニア部門で取り組んでいる開発生産性分析について紹介しました。自分自身、データ収集処理の開発、可視化を担当し、自分達の開発活動が 定量 データとして表現される面白さを感じました。 最後までご覧頂きありがとうございました。明日の記事の担当は検索エンジニアの竹田さんです。お楽しみに。 現在、 エニグモ ではこのような開発生産性に関わる取り組みを行っています。 興味のある方は以下の求人をご参照ください!! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、エンジニアの川本です。 主に BUYMA の決済・配送を担当しているチームでバックエンドの開発をしています。 この記事は Enigmo Advent Calendar 2023 の 20 日目の記事です。 個人開発でPlanetScaleという MySQL 互換のサーバーレスデータベースを使用しているのですが、特筆すべき仕様として外部キーのサポートがありません。 planetscale.com 外部キー制約はDBレベルで強い整合性を担保できる便利な手段ですが、PlanetScaleではその機能が利用できないので、アプリケーションレベルで整合性を担保する必要があります。 MySQL の外部キーのオプションにはいくつか種類がありますが、これらが使えない場合にアプリケーション側ではどのように担保すればよいのでしょうか? 今回は、 Rails を例にしてアプリケーション側で MySQL の外部キーに相当する機能をどのように担保できるのかを検証してみようと思います。 余談:PlanetScaleについて 最近PlanetScaleはベータ版で外部キーをサポートし始めましたが、残念ながら Hobbyプラン ではまだサポートされておりません。 PlanetScaleの基盤である Vitess はOnline DDL の機能を提供しており、それが原因で外部キーのサポートが長らく難しかったようです。 以下のドキュメントやブログには、PlanetScaleが外部キーをサポートできるようになるまでの背景や課題、そしてその克服方法についての詳細な情報が記載されています。興味がある方はぜひ読んでみてください。 外部キーのサポートが難しかった理由 Operating without foreign key constraints Online DDL: why FOREIGN KEYs are not supported 外部キーをサポートするための取り組み Foreign key constraints beta The challenges of supporting foreign key constraints 親子関係のテーブルを作成 まず親子関係にある、Parent, Childテーブルを作成してサンプルデータを入れる。 -- テーブル作成 mysql> CREATE TABLE parent ( -> id INT NOT NULL , -> PRIMARY KEY (id) -> ) ENGINE=INNODB; mysql> CREATE TABLE child ( -> id INT NOT NULL , -> parent_id INT NOT NULL , -> PRIMARY KEY (id) -> ) ENGINE=INNODB; -- テストデータをインサート mysql> INSERT INTO parent (id) VALUES ( 1 ), ( 2 ); mysql> INSERT INTO child (id, parent_id) VALUES ( 1 , 1 ), ( 2 , 1 ), ( 3 , 2 ); -- データ構造の確認 mysql> SELECT * FROM parent p JOIN child c ON p.id = c.parent_id; + ----+----+-----------+ | id | id | parent_id | + ----+----+-----------+ | 1 | 1 | 1 | | 1 | 2 | 1 | | 2 | 3 | 2 | + ----+----+-----------+ MySQL の外部キー制約 MySQL では以下4つの ON DELETE 副次句で指定できる参照アクションがあります。 ON UPDATE 副次句もありますが、今回は ON DELETE に限定することにします。 dev.mysql.com ON DELETE CASCADE 親テーブルから行を削除し、子テーブル内の一致する行を自動的に削除する。 -- ON DELETE CASCADEを指定して外部キー制約を設定 mysql> ALTER TABLE child -> ADD CONSTRAINT fk_parent -> FOREIGN KEY (parent_id) -> REFERENCES parent(id) -> ON DELETE CASCADE; -- parentのid = 1のレコードを削除する mysql> DELETE FROM parent WHERE id = 1 ; -- parent_id = 1のchildのレコードも削除されていることを確認できる mysql> SELECT * FROM child; + ----+-----------+ | id | parent_id | + ----+-----------+ | 3 | 2 | + ----+-----------+ ON DELETE SET NULL 親テーブルから行を削除し、子テーブルの外部キーカラムを NULL にする。 ※ この設定をするときは、 child の parent_id は NOT NULL にしない。 -- ON DELETE SET NULLを指定して外部キー制約を設定 mysql> ALTER TABLE child -> ADD CONSTRAINT fk_parent -> FOREIGN KEY (parent_id) -> REFERENCES parent(id) -> ON DELETE SET NULL ; -- parentのid = 1のレコードを削除する mysql> DELETE FROM parent WHERE id = 1 ; -- parent_id = 1のchildのレコードのparent_idはNULLになっていることを確認 mysql> SELECT * FROM child; + ----+-----------+ | id | parent_id | + ----+-----------+ | 1 | NULL | | 2 | NULL | | 3 | 2 | + ----+-----------+ ON DELETE RESTRICT or ON DELETE NO ACTION or 指定なし 親テーブルに対する削除操作は拒否されます。また、 ON DELETE RESTRICT or ON DELETE NO ACTION or ON DELETE 指定なし は同じ挙動になります。以下の例では ON DELETE 指定なし で例を示します。 -- ON DELETE指定なしで外部キー制約を設定 mysql> ALTER TABLE child -> ADD CONSTRAINT fk_parent -> FOREIGN KEY (parent_id) -> REFERENCES parent(id) -- parentのid = 1のレコードを削除する -- childにはparent_id = 1のレコードがあるので削除拒否される mysql> DELETE FROM parent WHERE id = 1 ; ERROR 1451 ( 23000 ): Cannot delete or update a parent row : a foreign key constraint fails (`myapp_development`.`child`, CONSTRAINT `fk_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`)) -- parentもchildも削除されていない mysql> SELECT * FROM parent p JOIN child c ON p.id = c.parent_id; + ----+----+-----------+ | id | id | parent_id | + ----+----+-----------+ | 1 | 1 | 1 | | 1 | 2 | 1 | | 2 | 3 | 2 | + ----+----+-----------+ Rails 側の実装方法 Rails では、Active Recordの dependentオプション を使用して、 MySQL の外部キー制約に相当する機能を実現できます。 dependentオプション は親レコードに対して ActiveRecord::Persistence#destroy が実行されたときに、紐ずいている子レコードに対して実行されるメソッドのことです。 ON DELETE CASCADE ON DELETE CASCADE に相当することは、 delete_all , destory , destory_async のいずれかで実現することができます。これら3つは全て最終的に実現できることは同じですが、それぞれで以下のように挙動の違いがあります。 delete_all delete_allは、parentに関連付けられたchildが一括で1つの SQL で削除します。 また、childに対して ActiveRecord::Persistence#delete が実行されるので、 ActiveRecord::Persistence#destroy 実行時に作用するbefore_destroyやafter_destroyといったコールバックや孫クラスのdependentオプションが実行されません。 そのため、単純に削除 SQL を実行するだけなので関連するchildが多い場合にはdestroyよりパフォーマンスが向上する可能性があリます。 class Parent < ApplicationRecord self .table_name = ' parent ' has_many :child , dependent : :delete_all end irb(main): 002 > parent = Parent .find( 1 ) irb(main): 054 > parent.destroy TRANSACTION ( 0 .7ms) BEGIN Child Delete All ( 1 .2ms) DELETE FROM ` child ` WHERE ` child ` . ` parent_id ` = 1 Parent Destroy ( 0 .7ms) DELETE FROM ` parent ` WHERE ` parent ` . ` id ` = 1 TRANSACTION ( 1 .9ms) COMMIT => #<Parent:0x0000ffffaf032050 id: 1> destroy destroyは、parentに紐づくchildを全て取得して1件ずつ削除します。 ActiveRecord::Persistence#destroy が実行されるため、before_destroyやafter_destroyなどのコールバックも実行され、孫クラスにあるdependentオプションも実行されます。 そのため、関連するchildが多いと発行される SQL も増え、コールバックの実行や孫クラスのdependentオプションの実行が多くなり、delete_allよりもパフォーマンスが低下する可能性があります。 class Parent < ApplicationRecord self .table_name = ' parent ' has_many :child , dependent : :destroy end irb(main): 002 > parent = Parent .find( 1 ) irb(main): 062 > parent.destroy TRANSACTION ( 0 .3ms) BEGIN Child Load ( 1 .0ms) SELECT ` child ` .* FROM ` child ` WHERE ` child ` . ` parent_id ` = 1 Child Destroy ( 1 .3ms) DELETE FROM ` child ` WHERE ` child ` . ` id ` = 1 Child Destroy ( 1 .1ms) DELETE FROM ` child ` WHERE ` child ` . ` id ` = 2 Parent Destroy ( 0 .9ms) DELETE FROM ` parent ` WHERE ` parent ` . ` id ` = 1 TRANSACTION ( 1 .2ms) COMMIT => #<Parent:0x0000ffffafdad888 id: 1> destroy_async destroy_asyncは、parentに関連する全てのchildを非同期で1件ずつ削除します。 紐づくchildが非常に多く、即時での削除を求められない場合に有効です。紐づくchildが多いと処理が最悪の場合は タイムアウト する可能性もあります。そのような場合、まずparentを削除してクライアントにレスポンスを速やかに返し、残りの紐づくchildは非同期で削除することで問題を解決できます。 class Parent < ApplicationRecord self .table_name = ' parent ' has_many :child , dependent : :destroy_async end irb(main): 002 > parent = Parent .find( 1 ) irb(main): 070 > parent.destroy TRANSACTION ( 0 .3ms) BEGIN Child Load ( 0 .8ms) SELECT ` child ` .* FROM ` child ` WHERE ` child ` . ` parent_id ` = 1 Parent Destroy ( 0 .8ms) DELETE FROM ` parent ` WHERE ` parent ` . ` id ` = 1 TRANSACTION ( 2 .0ms) COMMIT Enqueued ActiveRecord ::DestroyAssociationAsyncJob ( Job ID : 63fc4528-934a-405c- 9311 -7bee9fb706b1) to Async(default) with arguments : { :owner_model_name => " Parent " , :owner_id => 1 , :association_class => " Child " , :association_ids =>[ 1 , 2 ], :association_primary_key_column => :id , :ensuring_owner_was_method => nil } => #<Parent:0x0000ffffae761700 id: 1> irb(main): 071 > Performing ActiveRecord ::DestroyAssociationAsyncJob ( Job ID : 63fc4528-934a-405c- 9311 -7bee9fb706b1) from Async(default) enqueued at 2023 - 12 -16T09: 16 :43Z with arguments : { :owner_model_name => " Parent " , :owner_id => 1 , :association_class => " Child " , :association_ids =>[ 1 , 2 ], :association_primary_key_column => :id , :ensuring_owner_was_method => nil } Parent Load ( 3 .0ms) SELECT ` parent ` .* FROM ` parent ` WHERE ` parent ` . ` id ` = 1 LIMIT 1 Child Load ( 5 .1ms) SELECT ` child ` .* FROM ` child ` WHERE ` child ` . ` id ` IN ( 1 , 2 ) ORDER BY ` child ` . ` id ` ASC LIMIT 1000 TRANSACTION ( 0 .3ms) BEGIN Child Destroy ( 0 .9ms) DELETE FROM ` child ` WHERE ` child ` . ` id ` = 1 TRANSACTION ( 2 .0ms) COMMIT TRANSACTION ( 0 .3ms) BEGIN Child Destroy ( 1 .0ms) DELETE FROM ` child ` WHERE ` child ` . ` id ` = 2 TRANSACTION ( 1 .7ms) COMMIT Performed ActiveRecord ::DestroyAssociationAsyncJob ( Job ID : 63fc4528-934a-405c- 9311 -7bee9fb706b1) from Async(default) in 63 .81ms ON DELETE SET NULL nullify ON DELETE SET NULL に相当することは nullify で実現できます。 parentに紐づくchildのparent_idをnullに更新して、parentを削除しています。 class Parent < ApplicationRecord self .table_name = ' parent ' has_many :child , dependent : :nullify end irb(main):088> parent = Parent .find( 1 ) irb(main):090> parent.destroy TRANSACTION ( 0 .3ms) BEGIN Child Update All ( 5 .0ms) UPDATE ` child ` SET ` child ` . ` parent_id ` = NULL WHERE ` child ` . ` parent_id ` = 1 Parent Destroy ( 3 .3ms) DELETE FROM ` parent ` WHERE ` parent ` . ` id ` = 1 TRANSACTION ( 1 .3ms) COMMIT => #<Parent:0x0000ffffae66f9a0 id: 1> ON DELETE RESTRICT or ON DELETE NO ACTION or 指定なし ON DELETE RESTRICT または ON DELETE NO ACTION に相当することは、 restrict_with_exception または restrict_with_error のいずれかで実現することができます。これら2つは全て最終的に実現できることは同じですが、それぞれで以下のように挙動の違いがあります。 restrict_with_exception parentに紐づくchildが存在することを確認して、処理を ロールバック して ActiveRecord::DeleteRestrictionError という例外を発生させます。 class Parent < ApplicationRecord self .table_name = ' parent ' has_many :child , dependent : :restrict_with_exception end irb(main):088> parent = Parent .find( 1 ) irb(main):094> parent.destroy TRANSACTION ( 0 .6ms) BEGIN Child Exists ? ( 1 .0ms) SELECT 1 AS one FROM ` child ` WHERE ` child ` . ` parent_id ` = 1 LIMIT 1 TRANSACTION ( 0 .5ms) ROLLBACK / usr / local/bundle/gems/activerecord- 7.0 . 8 /lib/active_record/associations/has_many_association.rb: 16:in ` handle_dependency': Cannot delete record because of dependent child (ActiveRecord::DeleteRestrictionError) restrict_with_error parentに紐づくchildが存在することを確認して、処理を ロールバック してfalseを返します。 class Parent < ApplicationRecord self .table_name = ' parent ' has_many :child , dependent : :restrict_with_error end irb(main):088> parent = Parent .find( 1 ) irb(main):098> parent.destroy TRANSACTION ( 0 .5ms) BEGIN Child Exists ? ( 0 .6ms) SELECT 1 AS one FROM ` child ` WHERE ` child ` . ` parent_id ` = 1 LIMIT 1 TRANSACTION ( 0 .4ms) ROLLBACK => false 最後に ここまでの紹介で、 Rails アプリケーションで MySQL の外部キー制約の参照アクションを実現する手段が理解できました。 ただし、データ整合性が担保されるのは、外部キー制約に準拠したアプリケーションからの実行時に限られます。もし、同じDBを参照するが外部キー制約に準拠していないアプリケーションが存在する場合、どのような影響が生じますでしょうか? 外部キー制約のないアプリケーションからの実行により、データ整合性が維持されなくなる可能性があります。このような事態を避けるためには、できるだけDBレベルで整合性を担保する方が望ましいです。 Planet Scaleのような外部キー制約をサポートしていないDBでは、今回紹介したようなアプリケーションの実装が有効であるかもしれません。しかし、外部キー制約がサポートされているDBでは、DBレベルでの制御が安全であると言えるでしょう。 明日の記事担当はデータエンジニアリングチームです!お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
.blog-figure{ width: 70%; margin-left: auto; margin-right: auto; display: block; } .blog-footer{ background: rgb(193,54,47); } .footer-icon{ width: 2rem; display: inline-block; } .figure-caption{ text-align: center; color: goldenrod; font-size: 0.8rem; } .figure-source{ opacity: 0.7; font-size: 0.6rem; color: gray; } .greetings { font-size: 0.8rem; padding: 2rem 1rem; text-align: center; display: block; margin-top: 2rem; background: rgb(250,248,236) -webkit-gradient(linear, 100% 0, 0 0, from(#acacac), color-stop(0.5, #ffffff), to(#acacac)); background-position: -4rem top; /*50px*/ background-repeat: no-repeat; -webkit-background-clip: text; -webkit-text-fill-color: transparent; -webkit-animation-name: greetings; -webkit-animation-duration: 2.2s; -webkit-animation-iteration-count: infinite; -webkit-background-size: 4rem 100%; /*50px*/ } @-webkit-keyframes greetings { 0% { background-position: -4rem top; } 70% { background-position: 30rem top; } 100% { background-position: 12.5rem top; } } .blog-emphasis{ font-weight: 600; color: goldenrod; } .blog-parts{ margin: 1.2em 0 1.5em 0; color: #3c3c3c; text-align: justify; } .blog-notes{ font-size: 0.8rem; } .entry-categories > a{ color: #888888; } こんにちは、グローバルチームのエンジニアのFernandです。 この記事は Enigmo Advent Calendar 2023 の19日目の記事です。私は、15年近く フルスタ ックエンジニアとして働いてきました。さまざまなプロジェクトに関わってきましたが、、新しいチームに参加するたびに、技術的なスキルだけでなく、特にソフトスキルについても新しく学ぶことが多くあります。今回は、職場のミーティングで外国人として直面した課題を共有したいと思います。 (jump to English version) ある日、ITエンジニアの妻が彼に頼んで、「スーパーで牛乳を1パック買ってきて。もし卵があれば1ダース買ってきて」と言いました。そこでエンジニアは買い物に行き、言われた通りに行動し、帰宅しました。手には牛乳パックが12個ありました。 妻は驚愕しました 。何が間違いだったのでしょうか? プロジェクト・マネジメント協会(PMI)の報告によると、ビジネス目標を達成しないプロジェクトの半数以上は、効果的なコミュニケーションの欠如によるものとされています。実際に、不十分なコミュニケーションにより、1500億円を投じたプロジェクトの場合には 約100億円がリスクとなる のです。ビジネスでは、12個の牛乳パックが大きな損害を生み出してしまうかもしれないのです。 コミュニケーションの齟齬による経済的リスク ソース: ©2013 Project Management Institute, Inc. Pulse of the Profession In-Depth Report: The High Cost of Low Performance: The Essential Role of Communications, May 2013. PMI.org/Pulse 私が日本の企業で働き始めた当初、さまざまなチームとのコミュニケーションに苦労しました。私と同じように、日本に住む外国人の多くが直面する経験です。 外国人が日本で働くうえでぶつかる壁(言語の壁が半数以上) ソース: NikkeiAsia 私が最初に入社したスタートアップ企業では、お客様の満足度が最も重要であり、彼らを 神様のように対応すべき ということを教わりました。そのため、ミーティングに参加する際、私は静かに座り、より理解するために一層耳を傾ける必要がありました。私は日本語に自信がなかったため、質問をすることや間違いを犯すことを恐れていました。自分が話すことで、クライアントから私たちの会社がネガティブに評価されるのではないかという不安がありました。 しかしそれでも、各ミーティングの後に反省点を見直して、気づいたことを改善してから自信を持って質問をするようにしました。そして、 効果的なコミュニケーションを大事 にする日本のチームに参加する機会を得たおかげで、私はミーティング中に質問をする勇気を持ち、プロジェクトやクライアントの要件をより理解できるようになりました。日本人の同僚は忍耐と理解力をもって複雑な概念を簡素化するために時間をかけて説明を繰り返してくれました。 しかし、言語の壁は、誤解を招く障害となることもあることも私は理解しています。それにより、会話の流れが乱れ、自分の意見を正確に表現することが難しくなります。思いを正確に表現することへの取り組みは、話す人と聞く人の双方にとっても苦悩の源となっています。できるだけこれを避けるために、事前に読み込んで準備し、質問をまとめて整理することが大事です。 外国人のメンバーがミーティングをする場合、一人だけに発言の責任が押し付けられないように、協力し合いながら取り組みます。通常、そこでは、生産的に結果を出すために、 話の途中で割り込むことが歓迎され、許容されます 。曖昧さを解消するために、ア イデア を効果的に共有して質問をするこの戦略は時に混沌としたものになりますが、コミュニケーションの理解を促進し、より協力的で生産的な働き方を促進するのに非常に効果的だと感じています。誤解や心配事が生じた際に即座にそれらを明確化することは、コミュニケーションの質を向上させ、より協力的かつ生産的な職場環境を生み出します。参加者全員が議論に貢献することを促すことで、創造性が高まり、ア イデア が高められ、より豊かでダイナミックな成果が生まれます。 しかし、文化的な習慣やエチケットにも配慮することが重要です、特に話し手が日本人の場合です。このような場合、 割り込むことは失礼とされ 、質問はミーティングの最後まで取っておくことが通例です。そういったミーティングでは、私はより受動的なアプローチを取る傾向があります。ただし、質問や懸念事項を早急に解決させたい場合、日本語の能力に自信がなくても話すように努めています。日本語の能力に自信がなくても、ただ聞くだけに頼るのではなく、積極的な参加こそが助けを求めて理解を深める唯一の方法であることを心に留めています。言語の壁を乗り越え、異文化間のコミュニケーションの複雑さをより効果的に乗り越えるためには、 日本語の勉強を続けることが重要 です。 私は現在、さまざまな国のメンバーから成るチームに所属しています。私たちの主要なコミュニケーション手段は日本語ですが、必要に応じて英語も使用しています。さまざまな時差の課題にも取り組んでおり、私たちのマネージャーはその違いがミーティングのスケジュールに影響しないよう柔軟に対応してくださいます。彼女は流暢な英語と日本語を活かし、私たちの質問や懸念事項をサポートし、チーム内の円滑かつ効果的なコミュニケーションを容易にしてくれます。私は定期的に他のチームとも交流しなければなりません。日本語を使ってのコミュニケーションは難しいですが、貴重な機会であり、コミュニケーションスキルを実践し向上させるチャンスでもあります。しかし、通常業務に追われて、学習に費やす時間の確保がなかなか難しくなっています。プライベートと仕事、そして 言語学 習のバランスを取ることは大変な努力を要します。まるでジャグリングをするようなもので、集中して言語の練習だけに時間を割くことはなかなか難しいものです。 言語の不完全さや課題があっても、ミーティングでの同僚たちはそれを判断することはありません。むしろ、言語の壁にとらわれず、積極的に参加して貢献する姿勢は評価されるでしょう。しかし、質問をする際には、ただ質問を投げる、自己アピールのためにするということは避けるべきです。そのような行動は、ビジネス上での評判に悪影響を及ぼすだけでなく、私たちが維持しようとしている協力的な雰囲気にも支障をきたす可能性があります。 さらに、会議を進行する際は、コミュニケーション能力や自信のレベルに関係なく、全参加者が積極的に参加できる包括的で支援的な雰囲気を育むことも重要です。間違った質問や関連性のない質問をする人がいても批判するべきではないでしょう。そのような場合に他者を批判すると、プロジェクトが始まる前から失敗したも同然です。前述したように、効果的なコミュニケーションが生産性を向上させるという認識はとても重要です。研究によれば、効果的なコミュニケーションは生産性を最大25%向上させることができます。最終的には、プロジェクトの成功は効果的かつ効率的なコミュニケーション能力にかかっています。卵と牛乳の個数を間違えることもないことでしょう。不確実な点が生じた場合には、積極的に話して明確化することが重要です。積極的なリスナーであり、共感を持って接することを心がけましょう。英語や日本語の流暢さだけに焦点を合わせるのではなく、彼らの質問やア イデア の価値がプロジェクトを前進させ成功させる上でとても重要です。 (English version) Communication in Workplace An IT engineer was asked by his wife to go to the store and buy a carton of milk, and if there are eggs, buy a dozen. So the engineer goes shopping, does as she says, and returns home with 12 milk cartons. Oh boy , But what went wrong? According to the Project Management Institute (PMI) report, over half of projects that fail to meet business goals can be attributed to ineffective communication. In fact, poor communication puts approximately $75 million at risk for every $1 billion spent on projects. Well, it seems that one dozen cartons of milk can potentially result in a substantial financial loss. The amount at risk for every US$1 billion spent on a project. Source: ©2013 Project Management Institute, Inc. Pulse of the Profession In-Depth Report: The High Cost of Low Performance: The Essential Role of Communications, May 2013. PMI.org/Pulse When I first started my journey working at Japanese companies, I often struggled to communicate effectively with various teams. Unfortunately, this burden is not unique to me but a shared experience among many foreign residents in Japan. Barriers foreigners encounter when working in Japan. Language barrier is the most prominent obstacle. Source: NikkeiAsia Working in startup companies, I quickly learned that client satisfaction was held in the highest regard, often considering them as king or god (o-kyaku-sama wa kamisama desu) whose every request must be fulfilled. Therefore, when I joined meetings, I had to sit quietly and listen harder to understand the discussions. I feared asking questions and making mistakes because of my Japanese language proficiency. My company might be judged by the client whenever I attempt to speak up. I was able to manage this by researching sample implementations after each meeting and creating prototypes before confidently raising a question. Then, I had the opportunity to join a Japanese team that fostered effective communication . Encouraged by my supportive environment, I mustered the courage to ask questions during meetings, improving my comprehension of projects and clients' requirements. My Japanese colleagues displayed immense patience and understanding, taking the time to reiterate their explanations and simplify complex concepts, ensuring everyone understood the information. But don't get me wrong, despite the positive environment, the language barrier often presents obstacles that could lead to misunderstandings and misinterpretations. It disrupts the conversation flow and makes it challenging to express oneself fully. The struggle to articulate thoughts accurately has become a source of frustration for both the speaker and the listener. In order to avoid this as much as possible, I read and prepared beforehand, collating and organizing my questions. In situations where the speaker is a non-Japanese teammate, I collaborated, ensuring that the onus of speaking does not solely rest on one person. Usually, in meetings that consist of non-Japanese participants, interruptions are welcomed and accepted to foster a productive environment where ideas can be effectively shared, questions can be asked, and clarification can be provided. While this strategy may seem chaotic at times, I find that it is incredibly effective in facilitating comprehension and generating deeper insights. Obtaining immediate clarity on any misunderstandings or concerns enhances the quality of communication and promotes a more collaborative and productive work environment. By establishing a culture where all participants are encouraged to contribute to the discussion, creativity surges, and ideas are elevated, creating a richer and more dynamic outcome. However, I am also mindful of cultural norms and etiquette, particularly when the speaker is Japanese. In these situations, it would be considered rude to interrupt , and it is customary to reserve questions until the end of the meeting. I tend to adopt a more passive approach during such meetings. Nevertheless, if I do have pressing clarifications or concerns, I try to speak up regardless of my lack of confidence in my Japanese proficiency. I remind myself that the only way to seek assistance and gain a better understanding is by actively participating in the conversation rather than solely relying on attentive listening. However, continuing to study Japanese remains paramount in overcoming these language barriers and navigating the intricacies of cross-cultural communication more effectively. Currently, I am fortunate to be a part of a diverse team with members from different parts of the world. Although our primary mode of communication is Japanese, we also embrace the use of English when needed. Despite the challenges of various time zones, our manager has done an exceptional job ensuring that these differences do not impede our meeting schedules. With her fluency in English and Japanese, she adeptly accommodates our questions and concerns, facilitating smooth and effective communication within the team. I also have to interact with other teams periodically. Communicating using the Japanese language is challenging, but it also gives me valuable exposure and the chance to practice and enhance my communication skills. Though I have to admit that the demanding nature of daily life and work commitments has restricted the amount of time available for study. Balancing work, personal responsibilities and language learning can be a juggling act, making it quite challenging to allocate uninterrupted time for focused language practice. Ultimately, the success of our projects hinges on our ability to communicate efficiently and effectively. If uncertainty arises, it is crucial to speak up and seek clarification. Rest assured that in meetings, colleagues will not judge any language imperfections or challenges you may encounter while expressing yourself in Japanese or English. Instead, your willingness to participate and contribute despite any linguistic limitations will be appreciated and acknowledged. However, it is also essential to avoid asking questions solely for the sake of asking or to showcase your skills. Engaging in such behavior may have a detrimental impact on your professional reputation and hinder the collaborative atmosphere we strive to maintain. In addition, it is equally important for the meeting host to take accountability in fostering an inclusive and supportive meeting environment that encourages active participation from all attendees, regardless of their communication skills or confidence levels. It is critical to refrain from criticizing individuals who may ask questions that might seem incorrect or irrelevant. Criticizing others in such instances, the project had already failed even before it started. Be an active listener and practice empathy. Fluency in English or Japanese should not be the sole focus; instead, the value of their questions and ideas truly matters in moving the project forward and achieving success. May the spirit of the holidays fill your home with love and peace. 明日の記事の担当はSellチームの平井さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、 iOS エンジニアの池田です。 この記事は Enigmo Advent Calendar 2023 の 18 日目の記事です。 この記事では担当のプロジェクトにおいて日々の開発を快適にするために実施している細々したことについて紹介します。 私は BUYMA の iOS アプリ開発 を担当しています。 iOS アプリ開発 に限らずプロジェクトを進める中では、コアタスク(ここでは iOS アプリの開発作業と定義)とノンコアタスク(コアタスク以外の作業)が発生します。 開発の効率を上げるため、開発者の開発の快適性を上げるためにノンコアタスクは極力削っていきたいものです。 ここではノンコアタスクの中でもチケット管理、Pull Requestの管理に関わる部分について触れていければと思います。 現在のプロジェクト環境 現在の担当プロジェクト内では、チケット管理にJIRA、開発用のプラットフォームには GitHub を利用しているので、それらに関わる内容について触れていきます。 また、コミュニケーションツールとしてSlackを利用しているため、こちらとの連携も少し触れます。 JIRA 自動化 JIRAのチケット管理では、各チケットの開始日や終了日、担当者、チケットの作業状況など様々な情報の日々の更新が必要になってきます。 一つ一つの作業は作業時間も短く単純作業ではあるのですが、数が多くなると作業時間ももちろん累積されますし、何にも考えずにできるわけではないため、じわじわ地味に体力を削られます。 そういった作業を減らし、少しでも快適性を上げるためにはJIRAの「自動化」が有効です。 以下は私が担当するプロジェクトで設定している「自動化」のルール一覧です。 様々設定されていますが、この中でも「プルリク エス トがマージされる → 課題を完了にする」が結構気に入っています。 Pull Request のコードレビューを受けた後自分でマージする運用をしている場合、このルールが設定されていないと、以下のような手順が必要になります。 ブラウザで GitHub を開く GitHub の画面上でPull Requestの一覧を開く 該当のPull Requestを開く Pull Requestをマージする ブラウザでJIRAを開く JIRAのボード等でチケットを完了に トランジション させる これらの手順の中で、4.から 5.では別サービスへの切り替えが必要で、6.の作業では ドラッグ&ドロップ かオプションメニューからの選択が必要だったりで、キーボードからマウスへの操作の切り替えも余分に必要になります。 4.で作業に満足して 5.以降忘れることもあり、JIRAのカンバン管理者から完了/未完了の問い合わせが来たとしたらコミュニケーションコストもかかります。 これだけでも意外と積み重ねるとコストになるので、設定することによる快適度は上がっている感じがします。 Gitブランチ作成 もう一つJIRAの機能で細かいけどよく使っている機能がチケットのブランチ作成機能です。 以下チケットの画面キャプチャです。 チケットの「開発 > ブランチを作成 > GITで新しいブランチを作成してチェックアウト」の欄で、チケットIDが入ったブランチ名を自動生成してくれます。 また、入力欄の右側のボタンで クリップボード にコピーできるため、 CLI でブランチ作成の際にコピー&ペーストでブランチを作成することができます。 こちらを利用することでブランチ名を考える手間や、 CLI で入力する手間を省くことができます。 名前を考えるのって決めの問題なのですが、意外と考えるコストがかかる作業だと思っているので個人的にはすごく助かっています。 GitHub PULL_REQUEST_TEMPLATE.md Pull Request を作成して開発されている場合、設定している リポジトリ が多いかと思います。 本プロジェクトでは、以下のようなトピックをフォーマット化して記載するようになっており、記載時の手間を少し減らしています。 関連チケット Pull Request でやったこと 懸念点・注意点 相談事項 また、JIRAと GitHub が連携されるようになっているため、チケットのURLを記載するとJIRAのチケットに GitHub のPull Requestが連動するようになっています。 Slack SlackとJIRA、Slackと GitHub も連携されており、チケットやPull Requestの条件に応じた更新があったタイミングで通知が来るようになっています。 JIRAの通知 JIRAの通知設定は以下のようになっており、コメントやステータスの更新等通知が来るように設定されています。 GitHub の通知 GitHub の通知設定は以下のようになっており、コメントの見落としが少なくなるように comments:'channel' の設定をしているところが個別に設定した部分だったかと思います。 まとめ チケット管理、Pull Requestの管理に関わる部分についてノンコアタスクを削減するため細々やっていることをご紹介しました。 プロジェクトの環境によって合う、合わない部分があったりしますが、見て頂いた方の何かのご参考になれば幸いです。 日々の変化している状況や、気づいたちょっとした不要な手間など、アンテナを高くしつつさらなる改善、日々の開発の快適度を上げていければと思います。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター