TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

430

エブリーは2024年10月19日(土)に開催された Vue Fes Japan 2024 にゴールドスポンサーとして協賛させていただきました。 今回は参加レポートとして、会場の様子やセッションの感想についてお届けします。 イベント概要 Vue Fes Japanは、日本最大のVue.jsカンファレンスです。今年も多くの開発者が集まり、最新のVue.js関連技術や事例について学び、交流する機会となりました。 セッションの感想 1. キーノート Evan You氏によるキーノートセッションでは、Vue.jsエコシステムの最新動向と将来の展望について、深い洞察が共有されました。 主な注目ポイントは以下の通りです: Vueフレームワークの最新進展 Nuxt DevToolsの将来像 Viteビルドツールの現状と今後の方向性 現行のesbuild・Rollup・SWC構成から、RolldownとOxcへの移行戦略 OxcコンパイラとRolldownバンドラーの性能評価 これらのトピックを中心に、多岐にわたる内容が網羅されました。 個人的に、Evan You 氏が最近設立した Void Zero Inc. に非常に注目しています。 JavaScriptエコシステム全体のために、オープンソースかつ高性能な統合開発ツールチェーンの構築の実現によって、Vue.jsはもとより、JavaScript開発全般にもたらす可能性に大きな期待を寄せています。 2. Vue.js / Nuxt ハンズオン Vue Fes Japan では、毎年恒例のハンズオン企画として、Vue.js を学び始めたい方向けの教材を提供しています。今年は特別な取り組みがありました。 Nuxt の公式チュートリアル「Nuxt Tutorial」の作者である Anthony Fu 氏と Vue Fes Japan のコラボレーションにより、この公式チュートリアルの日本語版が先行公開されました。このチュートリアルがハンズオン企画の題材として使用されました。 内容は Vue.js の基礎(リアクティビティ、Composition API など)から始まり、Nuxt のコアなコンセプトまでが網羅されていました。 これから Vue.js・Nuxt を学び始めたい方には、このチュートリアルを通じて、より深い理解を得ることができると思います。 learn-nuxt.vuejs-jp.org 3. 次世代フロントエンドクロストーク 次世代フロントエンドクロストークセッションでは、JavaScriptエコシステムの最新動向と課題について活発な議論が展開されました。 主な注目ポイントは以下の通りです: フロントエンドビルドツールの進化 Viteが Vue や React SPA のデファクトスタンダードとして定着 Rust製ツール(Oxcコンパイラ、Rolldownバンドラーなど)の台頭 JavaScriptエコシステムのRust化の加速 AIによる大規模コード生成の可能性と課題 これらのトピックを通じて、フロントエンド開発の未来像について多角的な視点が提示されました。 特にRustの重要性が強調されたことで、私自身もRust学習への意欲が大いに刺激されました。このセッションを通じて、フロントエンド開発の将来がより鮮明に見えてきたと感じています。 スポンサーブース紹介 エブリーでは DELISH KITCHEN Web や DELISH KITCHEN チラシ などで Vue.js を採用しています。 いつも Vue コミュニティの恩恵を受けている我々もコミュニティのさらなる盛り上がりに貢献してくべく、スポンサーとして協賛させていただき、ブースも出展させていただきました! ブース エブリーでは、今回も弊社が提供するDELISH KITCHENのサービスをイメージしたブースの雰囲気を作りました。多くの方からDELISH KITCHENをを使っていますとの声をかけていただき、とても嬉しかったです。 ノベルティ 今回もDELISH KITCHENにちなんだノベルティを用意させていただきました。 ステッカー DELISH KITCHENグッズ CTOブレンドのコーヒーバッグ DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。参加者の方々にも好評で多くの方に参加していただけました! アンケート 今回、アンケートでは「Vue について教えて! 」と題して、「Vue の好きなところ」、「Vue の苦労したところ」について回答してもらいました。今回のアンケートでは付箋に自由に記述っしてもらう形式を取り、多くの方から様々な意見をいただくことができました。 回答いただいた多くの皆様、ありがとうございました! 各社スポンサーブースの様子 会場の1階にはスポンサーブースが展開され、各社の趣向を凝らしたブースに多くの人が足を止めていました。 どのブースも、それぞれの会社の特徴を生かした面白い展示が行われており、飽きることなく見て回ることができました。 GMO インターネットグループさんのブースでは、3種類の生成 AI を使って 天秤.AI by GMO の Web 画面を Vue.js で出力させた実装と実際の画面を展示して、好きな出力結果のアンケートを行っていました。 生成 AI の利用はとてもホットなトピックなので興味深かったです。筆者の好みは GPT-4 の出力結果でした! MedPeer さんのブースでは、「握力で技術的負債を粉砕しよう!」と題して、握力測定をすることでノベルティをもらえるという企画を行っていました。握力測定ができるブースは初めて見たのでとても新鮮でした。ちなみに、筆者は 43.6 kg という結果で、無事に学校で体力測定をしていた頃の過去の自分を超えることができました! まとめ Vue Fes Japan 2024 にゴールドスポンサーとして協賛できたことを光栄に思います。このイベントを通じて、Vue.js コミュニティの発展に寄与できたことは、私たちにとって大きな喜びです。 多くの方々にエブリーのブースにお立ち寄りいただき、Vue.js の最新トレンドやエブリーのサービスについて活発な議論を交わせたことに、心から感謝申し上げます。皆様との対話は、私たちにとっても大変刺激的で有意義な経験となりました。 今回のイベントでの経験を糧に、エブリーは今後も Vue.js コミュニティのさらなる発展に貢献していく所存です。Vue.js の最新情報やベストプラクティス、そしてエブリーのサービスを通じた実践的な知見を、継続的に発信してまいります。
はじめに エブリーでソフトウェアエンジニアをしている本丸です。 最近、Amazon Cognitoのユーザープールから別のユーザープールにユーザーを移行する方法について調査する機会がありました。 Amazon Cognitoに関しては色々な箇所で使われていると思いますが、ユーザーの移行について触れる機会はそれほど多くないかと思いますので紹介しようかと思います。 Amazon Cognitoとは Amazon Cognito(以降Cognitoと表記します)は、AWSが提供するウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。ユーザーの認証・承認を行うユーザープールとユーザーにAWSリソースへのアクセスを許可するアイデンティティプールを持っています。 DELISH KITCHENでは、ユーザーのメールアドレスの管理とメールアドレスを用いたサインインにCognitoのユーザープールを利用しています。 Lambdaトリガー CognitoにはLambdaトリガーという機能があり、ユーザープールに対してサインインなどのイベントが発生した時に、それをトリガーとしてLambdaを呼び出すことができます。 公式ドキュメントからの引用ですが、Lambdaトリガーとして設定できるイベントには下記のようなものがあります。 トリガーの種類 説明 認証前の Lambda トリガー サインインリクエストを承認または拒否するカスタム検証 サインアップ前の Lambda トリガー サインアップリクエストを承認または拒否するカスタム検証を実行する ユーザー移行の Lambda トリガー 既存のユーザーディレクトリからユーザープールにユーザーを移行する カスタムメッセージの Lambda トリガー メッセージの高度なカスタマイズとローカライズを実行する Cognitoのユーザープールへのインポート ユーザープールへのインポート・移行方法は2つ用意されています。CSVを用いた方法とLambdaトリガーを用いた方法です。 CSVを用いたインポート CSVを用いたインポートでは、指定されたフォーマットのCSVファイルを使用して一括でユーザーのインポートを行います。公式ドキュメントでは低労力で低コストなオプションとして紹介されていました。 こちらの方法では、セキュリティの観点からパスワードのインポートができないようになっています。そのため、移行の際にユーザー側でパスワードの変更が必要になります。 Lambdaトリガーを用いたインポート Lambdaトリガーを利用したインポートでは、前述したLambdaトリガーを起点にユーザーの移行を行います(上述の表の ユーザー移行の Lambda トリガー が今回説明するトリガーです)。このトリガーは、ユーザーがサインインする時とパスワードのリセットを行うときに発火します。 こちらの方法では、パスワードも連携されるのですが認証フローに USER_PASSWORD_AUTH または ADMIN_USER_PASSWORD_AUTH を指定し、ユーザー名とパスワードによる認証を行わなければならない点に注意です。 少しイメージしにくいかと思うので、次でもう少し詳細に説明します。 Lambdaトリガーを用いたユーザーの移行の実装 Lambdaトリガーを用いたユーザー移行の流れはおおよそ図のようになります。 ユーザーを移行したい先のアプリケーションでサインインもしくはパスワードリセットが呼び出されたのをトリガーにしてユーザー移行のLambda(図の user migration lambda )が呼び出されます。 ユーザー移行のLambdaの実装は次のようになります。 import { CognitoIdentityProviderClient, AdminInitiateAuthCommand, AdminGetUserCommand, CognitoIdentityProviderClientConfig, UserNotConfirmedException } from "@aws-sdk/client-cognito-identity-provider" ; import { UserMigrationTriggerHandler } from "aws-lambda" ; const userMigration: UserMigrationTriggerHandler = async ( event ) => { const config: CognitoIdentityProviderClientConfig = { region : 'ap-northeast-1' , } ; const client = new CognitoIdentityProviderClient(config); if (event.triggerSource == "UserMigration_Authentication" ) { const adminInitiateAuthCommand = new AdminInitiateAuthCommand( { UserPoolId : ${ USER_POOL_ID } , ClientId: $ { CLIENT_ID } , AuthFlow: "ADMIN_USER_PASSWORD_AUTH" , AuthParameters: { "USERNAME" : event.userName, "PASSWORD" : event.request. password , } , }); // 認証できるかチェック try { await client. send (adminInitiateAuthCommand) } catch (e) { console .log( `user auth failed: ${ e.message } ` ); throw e; } // cognitoに登録するユーザー情報構築 const adminGetUserCommand = new AdminGetUserCommand( { UserPoolId : process .env.DK_USER_POOL_ID, Username : event.userName, } ); try { const response = await client. send (adminGetUserCommand); // 移行先のユーザーに持たせたい情報を詰め込む event. response .userAttributes = { "email" : response.UserAttributes. find (( attr ) => attr.Name === "email" )?.Value ?? "" , "email_verified" : response.UserAttributes. find (( attr ) => attr.Name === "email_verified" )?.Value ?? "false" , } ; // 検証メールを送信しないため、下記を指定する event. response .messageAction = "SUPPRESS" ; event. response .finalUserStatus = "CONFIRMED" ; return event; } catch (e) { console .log( `get user failed: ${ e.message } ` ); throw e; } } return event; } ; このユーザー移行のLambdaの中で、移行元となるCognitoでユーザーの認証を行い、認証が成功した場合にユーザーの情報を取得します。その情報を移行先のCognitoに返すことでユーザーの移行を行います。 event.response に渡すデータを変更することで移行先のユーザーに持たせたい情報を変更したり、ユーザーがそのメールアドレスの正当な所有者であるかを確認するメールを送信するかなどを操作することができます。 まとめ Cognitoのユーザーの移行方法を調査して、ユーザーの移行を行うためにAWS公式で便利な機能が用意されていることを知ることができました。 2つの方法にそれぞれメリット・デメリットがあるかと思うので適切に使うようにしていきたいと思います。 参考資料 https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-import-users.html
こんにちは、DevEnableグループの 羽馬 です。 いよいよ日本最大のVue.js開発者カンファレンス「Vue Fes Japan 2024」の開催が目前に迫りました! 10月19日に開催されるこのイベントは、Vue.jsエコシステムの最新トレンドや先端技術が一堂に会する、開発者必見の祭典です。 エブリーは今年初めてゴールドスポンサーとして協賛し、Vue.jsコミュニティの皆様との交流を心待ちにしています。 この記事では、エブリーのブース内容の詳細や、カンファレンスの前後に開催するVue Fes Japan 2024関連イベントについてご紹介します。Vue.jsに興味がある方、エブリーの技術や文化に触れてみたい方は、ぜひ最後までお読みください! エブリーのブース紹介 エブリーのブースでは、開発組織やサービスの魅力を存分に感じていただけるよう、様々な工夫を凝らしています。 魅力的なノベルティ DELISH KITCHEN のオリジナルキッチングッズ 先着限定オリジナルタンブラー CTOオリジナルブレンドコーヒーバッグ 特別企画:every CTO Blend 今回特に注目いただきたいのが、オリジナルでブレンドしたコーヒー「every CTO Blend」のコーヒーバッグです。 CTOが厳選したブレンド豆使用 香り高い味わいをそのままコーヒーバッグに コーヒーバッグ制作の裏話や想いは、以下の記事で詳しくご紹介しています。ぜひご覧になってください! 人々へ明るい変化を提供する、オリジナルブレンドコーヒー「every CTO Blend」を制作 エブリーのブースにはお気軽にお立ち寄りください。技術の話題はもちろん、エブリーの魅力や開発文化について、私たちスタッフが熱意を持ってお伝えします。皆様とお会いできることを心より楽しみにしています。 Vue Fes Japan 2024関連イベントのご案内 Vue Fes Japan 2024の前後に、協賛企業の皆様と共催して特別イベントを開催します。これらのイベントは、Vue.jsコミュニティの交流をさらに深める絶好の機会です。 Vue Fes Japan 2024 Pre LT Party Vue Fes Japan 2024 After Night 両イベントとも公募LT枠をご用意していますので、ぜひご参加ください! さいごに Vue Fes Japan 2024は、Vue.js開発者コミュニティにとって貴重な学びと交流の場です。エブリーは、このイベントを通じて日本のVue.js開発の発展に貢献できることを光栄に思います。 Vue.jsに関心のある方、エブリーの技術や文化に興味をお持ちの方、ぜひVue Fes Japan 2024にご参加ください。エブリーのブースでお会いできることを心よりお待ちしています!
目次 はじめに 自動生成しようと考えた背景 トモニテが公開しているLPについて 既存のLP実装の課題 生成AIによるLP実装の自動生成 LP自動生成のためのアプローチ 実際に自動生成してみて OpenWebUI を利用した場合 LlamaIndex.TS を利用した場合 そのほかのアプローチ まとめ はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 今回の記事では、生成AIを利用してトモニテが公開しているLPの自動生成に挑戦してみた時の話をしたいと思います。 (※2024年7月下旬〜8月上旬時点での話になります。) 自動生成しようと考えた背景 まずはじめに、なぜLPの自動生成をしようと考えたのか、その背景を説明したいと思います。 トモニテが公開しているLPについて トモニテでは、企業様と提携したLPを複数公開しています。 LPの例 これらのLPは、TypeScript/React/Next.js を利用して実装されており、microCMS 1 を利用してコンテンツを管理しています。 各LPは静的ページとしてビルドされ、S3 にデプロイ、CloudFront を経由して配信されます。 また、各LPの仕様書は SpreadSheet/Figma にて管理されており、各LPの実装は仕様書を元に行われています。 基本的には、ビジネスサイドが仕様書を作成し、デザイナーがデザインを作成し、エンジニアが実装を行うというようなフローとなっています。 既存のLP実装の課題 全てのLPで共通しているパラメータや画像パーツなどのコンテンツは、microCMS を利用して設定できるようになっています。 また、各LPで利用するフォーマットやコンポーネント(プルダウン、選択パネルなど)は一定共通化されていますが、それぞれのLPに合わせて設問や表示方法などが異なっています。 そのため、新しいLPを作成する際には、既存のLPをコピーした後で仕様書を元に細かいチューニングを行う形で開発が行われています。 事業の拡大を目指す中で、LPの数も増えていき、LPの開発・運用に要する人的/時間的リソースの増加がジワジワと事業促進におけるボトルネックとして感じられるようになってきています。 そこで、エンジニアの開発工数やビジネスサイドの確認工数を削減し、事業のスピードアップを図るために生成AIを利用したLPの自動生成を検討しました。 生成AIによるLP実装の自動生成 LP自動生成のためのアプローチ 今回検討した生成AIによるLPの自動生成では、以下のようなフローをイメージしました。 既存のLPの仕様書および実装を AI モデルに embedding で学習させる。 新規LPの仕様書およびプロンプトを AI モデルに入力し、LPの実装を生成する。 生成AIを利用するにあたって、詳細なモデルを決定する以前に、ChatGPT 2 のようなオープンなモデルを利用するのかローカルLLMを利用するのかという観点があります。 ローカルLLMはモデルの性能が低めであったり実際に使用しているマシンのスペックが影響したりするため精度は落ちがちですが、今回は新しくコストをかけたくないという要望があったので、ローカルLLMを利用することにしました。 実際に自動生成してみて 実際にLPの自動生成を行ってみた時のことについて説明したいと思います。 ローカルLLMに関しては、様々なモデルをシンプルに利用することのできる Ollama 3 を採用しました。Ollama は、モデルの重みを量子化して推論を高速化することで CPU レベルのスペックでも LLM を扱えるようにした Llama.cpp 4 をラップしてローカルでも動かせるようにしてくれるツールです。 ちなみに、 Ollama で利用できるモデルは こちら から確認することができます。 実際に Ollama を組み込むために、以下の2つのパターンを試してみました。 Open WebUI 5 (ローカルで動かせる ChatGPT のような AI チャットツール) LlamaIndex.TS 6 (LLM を組み込んだアプリケーションを開発するための TypeScript 向けフレームワーク) OpenWebUI を利用した場合 Open WebUI + Ollama による LLM 開発環境を構築できるコンテナイメージが公開されているため、以下のコマンドで簡単にローカル環境を構築できます 7 。 $ docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama 上記のコマンドでコンテナを起動しておいてブラウザから http://localhost:3000 にアクセスするとOpen WebUI を利用した AI チャットツールが利用できます。 設定画面から Embedding のようなモデルの設定などが行えます。 Open WebUI のチャット画面( Open WebUI 公式サイト ) モデルやプロンプトに関しては色々と調整してみましたが、以下のプロンプトを llama3.1 に渡した時に一番良さそうなレスポンスが返ってきました。 プロンプト `ExistingLP.tsx` was created referencing `existing_lp_specification.csv`.\nLike that, create `NewLP.tsx` referencing `new_lp_specification.csv`.\nYou must show me full implementation. 生成された実装 import React, { useState, useEffect } from 'react' ; import { useForm } from 'react-hook-form' ; import { useRouter } from 'next/router' ; import EntryAPI from '../api/EntryAPI' ; export default function NewLP ( { campaign } : Props ) { const [ isError , ToggleError ] = useState( false ); const [ isSubmit , ToggleSubmit ] = useState( false ); const router = useRouter(); const methods = useForm( { shouldFocusError : false } ); const watch = methods. watch (); const entryAPI = new EntryAPI(); const onSubmit = ( data : DoNotUseAnyType ) => { ToggleSubmit( true ); const refererParams = getCommonEventParams(); const answers = { email : data.email, gender : data.gender, ... // レスポンスではコードが記述されていましたが省略しています // Additional fields from the CSV file venue : data.venue, experience : data.experience, } ; entryAPI.submitEntry(answers, refererParams) . then (( response ) => { console .log(response); router. push ( '/thank-you' ); } ) . catch (( error ) => { ToggleError( true ); console .error(error); } ); } ; return ( < div > { isSubmit ? ( 'Thank you for submitting your entry!' ) : ( < form onSubmit ={ methods . handleSubmit ( onSubmit ) } > ... // レスポンスではコードが記述されていましたが省略しています < label > Email: < input type = "email" name = "email" { ... methods . register ( 'email' ) } /> </ label > < label > Gender: < select name = "gender" { ... methods . register ( 'gender' ) } > { genders . map ( (gender ) => ( < option key ={ gender } value= { gender } > { gender } </ option > )) } </select> </ label > ... // レスポンスではコードが記述されていましたが省略しています <button type = "submit" >Submit</ button > </form> )} </div> ); } とりあえず画面を表示するくらいはできそうな実装が返ってきました。 実際のLPの実装や仕様書をお見せできないので伝わりにくいのですが、既存の実装を踏襲できていない部分や仕様書の内容を反映できていない部分が多くありました。 LlamaIndex.TS を利用した場合 次に、LlamaIndex.TS を利用してローカルで LLM を利用する方法を試してみました。 既存のLPがTypeScriptで実装されているので、組み込みやすいように TypeScript 向けのフレームワークを利用することにしました。 LlamaIndex.TS を利用して TypeScript で Ollama を動かす方法に関しては公式のチュートリアル 8 が参考になります(Node.js v18からしか対応していません 9 )。 ここでは、新しいページを作成して、そのページでLLMとのやりとりを行えるようにしようと考えました。 実際の画面や詳細は割愛しますが、このやり方でもあまり良い結果は得られませんでした。 新しいページでLLMを動かすための実装 結果が芳しくなかったので、クエリをページ上から入力できるようにすることまではしていません。 LLM とのチャットページの実装 ( src/pages/llm.tsx ) import { useState } from "react" ; const LpGenerator = () => { // ボタンをクリックするとmain関数が実行されるページ const [ query , setQuery ] = useState( "What did the author do in college?" ); const [ result , setResult ] = useState( "" ); const onClick = async ( e : any ) => { e. preventDefault (); const response = await fetch ( '/api/query_llm' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , body : JSON . stringify ( { query } ), } ); const data = await response.json(); setResult(data. result ); console .log(result); } return ( <> < p >LP Generator </ p > < button onClick = {onClick} >Run LLM Query</ button > { result && < p >{ result } </ p >} </> ) } ; export default LpGenerator; API ハンドラーの実装 ( src/pages/api/query_llm.ts ) import type { NextApiRequest, NextApiResponse } from 'next' ; import QueryLLMAPI from 'api/query_llm' ; export default async function handler ( req : NextApiRequest , res : NextApiResponse ) { const { query } = req. body ; const queryLLMAPI = new QueryLLMAPI(); const result = await queryLLMAPI.post(query); res. status ( 200 ).json( { result } ); } LLM とのやりとりを行う API の実装 ( src/api/query_llm.ts ) import fs from "fs/promises" ; import { Document , HuggingFaceEmbedding, Ollama, Settings, VectorStoreIndex } from 'llamaindex' ; interface IQueryLLMAPI { post ( query : string ): Promise < string > ; } export default class QueryLLMAPI implements IQueryLLMAPI { constructor () { Settings.llm = new Ollama( { model : "llama3.1:8b" } ); Settings.embedModel = new HuggingFaceEmbedding( { modelType : "BAAI/bge-small-en-v1.5" , quantized : false } ); } async post ( query : string ): Promise < string > { // テキストの読み込みと処理 console .log( "Reading text..." ); const path = "node_modules/llamaindex/examples/abramov.txt" ; const essay = await fs.readFile(path, "utf-8" ); // Documentの生成とインデックスの作成 console .log( "Creating document and index..." ); const document = new Document ( { text : essay, id_ : path } ); const index = await VectorStoreIndex.fromDocuments( [ document ] ); // クエリエンジンでクエリを実行 console .log( "Querying..." ); const queryEngine = index.asQueryEngine(); const response = await queryEngine.query( { query : query } ); // 結果をクライアントに返す return response. toString (); } } そのほかのアプローチ ローカル LLM である Ollama を利用する以外にも、GitHub copilot や OpenAI を利用したアプローチも検討しました(弊社では既に利用可能な状況だったため、一旦「新しい」コストは発生していないという体で考えていました)。 しかし、残念ながら、これでも工数改善に繋がるようなクオリティの生成結果を得ることはできませんでした。 まとめ 今回の記事では、生成AIを利用してトモニテが公開しているLPの自動生成に挑戦してみた時の話をしました。 結果としては、今回の検証では、生成AIを利用してLPの自動生成を行うことは難しいという結論に至りました。 しかし、仕様書のフォーマットや既存実装の渡し方、推論プロセスなど改善できそうな点はまだあると思っています。 大きめの実装を実際に運用できるようなクオリティで生成させることは簡単ではないかと思いますが、今後の AI 技術の発展も含めて、引き続き期待したい領域だと感じました。 LP運用に関しては、引き続き改善を進めていきたいと考えているので、進展があった時にはまた記事にできたらと思います。 最後まで読んでいただき、ありがとうございました。 microCMS ↩ ChatGPT ↩ Ollama ↩ Llama.cpp ↩ Open WebUI ↩ LlamaIndex.TS ↩ Open WebUI + Ollama 開発環境のセットアップ ↩ LlamaIndex.TS チュートリアル ↩ LlamaIndex.TS yarn パッケージ ↩
レシピメディアにおいて、たとえば検索数推移のような時系列的なデータを扱っていると、急激に検索数が伸びているワードを捕捉したいシーンがあります。 要因はものによって違いますが、これをSQLだけで完結してなるべく楽したい。が今回の目的です。 要するに異常検知をすることが目的なのですが、「上昇率がX%以上を検知する」ような単純なモデルではないが、ある程度統計的な根拠をもとに検知が可能で、かつPythonのライブラリをつかうほど複雑ではなくSQL上でわかりやすく書けることを主眼に置きます。 方針としては以下です。 特定ワードにおける検索数の時系列データが正規分布に従うと仮定する 特定ウィンドウにおける検索数の移動平均と、標準偏差を抽出し、有意水準を5%などとし、逸脱したものを異常値とみなす イメージはこんな感じです。各週ごとに適正な範囲を求めて、そこを逸脱した値を異常値としてみなします 本来は標準化すべきですが、簡単のため標準化処理は行わずに進めます。 と、いうことで早速やっていきます。異常検知までの全体的なステップは以下です。 特定ワードにおける週ごとの検索数を抽出する 4週間ごとの検索数の移動平均を抽出 4週間ごとの検索数の標準偏差(σ)を抽出 移動平均±2σを抽出 現在週の値を判定する まず今回想定するデータソースはこんな感じです。仮に search_data というテーブルに入っているとして進めて行きます。 date: 日付 query: 検索ワード num_of_searches: 検索数 まず特定ワード xxx の検索数を抽出します。今回は最終的に週ごとの移動平均を取りたいので、週ごとの集計にします。 なお実行環境はPrestoとします。適宜ご自身の扱っているSQLに読み替えてご確認ください。 ▼検索数の抽出 with weekly_data as ( select week(date) as week , sum(num_of_searches) as total_searches from search_data where q = 'xxx' group by week ) 次に移動平均と標準偏差を抽出します。ウィンドウ関数を使います。 ▼移動平均と標準偏差の抽出 stats as ( select week total_searches , avg (total_searches) over ( order by week rows between 4 preceding and 1 preceding row ) as moving_average , stddev (total_searches) over ( order by week rows between 4 preceding and 1 preceding row ) as moving_stddev from weekly_data ) 次に移動平均±2σを抽出します。先程のクエリで同時に計算してもいいですが、若干わかりづらくなるので、こちらで移動平均±2σを抽出します。これにより、異常値の範囲を明確にすることができます。 ▼移動平均±2σの抽出 , bounds as ( select week , total_seaches , moving_average , moving_stddev , moving_average + 2 * moving_stddev as upper_bound , moving_average - 2 * moving_stddev as lower_bound from stats ) これで準備ができました。過去4週間の移動平均±2σと今週の値を見比べてみましょう。現在週の検索数がこの範囲を超えているかどうかを確認することで、異常値を検知することができます。 ▼異常検知する select total_searches > upper_bound or total_searches < lower_bound as is_anomaly from bounds これで異常値を検出できます! 今回のクエリを実行することで、現在の検索数が過去のトレンドから逸脱しているかどうかを簡単に確認できます。今回はSQLでやりましたが、どちらかと言うとスプレッドシートのほうがより簡単にできそうな気もしてきました。スプレッドシートでは、関数を使って同様の計算を行うことができるため、視覚的にデータを確認しやすいという利点があります。 是非ご自身で試してみてください。データの異常検知は、ビジネスの意思決定において非常に重要な要素ですので、さまざまな手法を試してみることをお勧めします。
はじめに こんにちは!トモニテにて開発を行っている吉田です。 今回は API を本番のデータにつなぎながら確認できるようステージング環境を作成したのでそのことについて書いていきます! 目的 本番環境へのリリース前には、さまざまなケースを考慮したテストを行うことが不可欠です。しかし、開発環境で考えられる限りのケースを網羅しても、どうしても考慮漏れが発生することがあります。このようなリスクを軽減するために本番環境を利用したステージング環境を構築しました。 これにより、実際のデータを使用しながら運用時を想定したテストを行うことが可能になりリリース前に潜在的な問題を早期に発見し、サービスの品質を向上が期待できます。 ステージング環境の作成 現在のトモニテのインフラ構成は簡単に図に起こすと下記のようになっています。 今回は ECS を新たに作成しリスナールールを追加することでステージング環境の構築を実現をします。 図だと赤枠で囲っているところが新たな構成になるイメージ(下図)です。 ステージング環境の作成でのゴールは以下のように設定しました。 エンドポイントを叩いて動作確認ができる 開発ブランチへのマージをトリガーにステージング環境のサーバーにもデプロイ エンドポイントを叩いて ステージング できるようにする 弊社ではインフラの管理に terraform を利用しています。以下は、AWS インフラを terraform を用いて構築するためのリソース定義のサンプルです。 (各リソースについて一部省略しています) 1.タスク定義 resource "aws_ecs_task_definition" "stg_server" { family = "stg-server" requires_compatibilities = [ "FARGATE" ] network_mode = "awsvpc" cpu = 256 memory = 512 execution_role_arn = <タスク実行のARN> task_role_arn = <IAMロールのARN> // 他のAWSサービスを呼び出すため container_definitions = jsonencode ( [ { command = [ ... ] cpu = 0 // 指定がなければ自動で割り当てられる environment = [ ... ] essential = true // タスク内のいずれかのコンテナが停止したときにすべてのコンテナを停止するか image = <ecrのイメージを指定> logConfiguration = { logDriver = "awslogs" options = { awslogs-create-group = "true" awslogs-group = "stg-server" awslogs-region = <region指定> awslogs-stream-prefix = "ecs" } } mountPoints = [] name = "stg-server" portMappings = [ { containerPort = 1323 hostPort = 1323 protocol = "tcp" } , ] secrets = [ ... ] volumesFrom = [] } , ] ) runtime_platform { cpu_architecture = "X86_64" operating_system_family = "LINUX" } } 2.ターゲットグループ resource "aws_lb_target_group" "stg_server_target" { name = "stg_server_target" port = 1323 target_type = "ip" protocol = "HTTP" vpc_id = <VPCのID> deregistration_delay = <ドレインするまでに待機する時間> health_check { path = "/healthcheck" interval = 30 timeout = 5 healthy_threshold = 5 unhealthy_threshold = 2 } } 3.クラスター resource "aws_ecs_cluster" "stg_ecs_cluster" { name = "stg_ecs_cluster" } 4.サービス resource "aws_ecs_service" "stg_server" { name = "stg-server" cluster = aws_ecs_cluster.stg_ecs_cluster.arn task_definition = "stg-server" desired_count = 1 deployment_maximum_percent = 200 deployment_minimum_healthy_percent = 100 launch_type = "FARGATE" enable_execute_command = true load_balancer { target_group_arn = aws_lb_target_group.stg_server_target.arn container_name = "stg-server" container_port = 1323 } network_configuration { subnets = <subnet指定> security_groups = <セキュリティグループ指定> assign_public_ip = true } lifecycle { ignore_changes = [ task_definition ] } } 5.ロードバランサー resource "aws_alb_listener_rule" "stg_server_rule" { listener_arn = <登録するリスナーリソース>.arn priority = <優先度> action { type = "forward" target_group_arn = aws_lb_target_group.stg_server_target.arn } condition { host_header { values = [ <マッチするホストヘッダーパターン> ] } } } 6.Route53 レコード resource "aws_route53_record" "stg_server" { name = <レコード名> records = <ELBのDNS名> ttl = "300" type = "CNAME" zone_id = <ホストゾーンのID> weighted_routing_policy { weight = 100 } } 7.ECR のライフサイクルポリシー resource "aws_ecr_lifecycle_policy" "stg-server-lifecycle_policy" { repository = <stg環境用のECRリポジトリ>.name policy = <<EOF { "rules": [ { "rulePriority": 1, "description": "最新の1イメージを残す", "selection": { // 今回はタグによる指定 "tagStatus": "tagged", "tagPrefixList": ["stg"], "countType": "imageCountMoreThan", "countNumber": 1 }, "action": { "type": "expire" } } ] } EOF } 上記の構成を既存リソースに加えることで新規に作成した API についてエンドポイントを叩いて動作確認することができるようになりました。 ※今回は認証について言及していませんが別途認証の設定等も必要になります。 注意点 ALB やホストゾーンなど既存のリソースを利用しているものについては記述を省略しています。 <...>で囲まれた部分は、実際の値に置き換えてください。 各リソースの設定は、具体的な要件や環境に応じて調整が必要です。 Terraform のバージョンや AWS プロバイダーのバージョンによって、リソースの属性や構文が異なる場合がありますので、公式ドキュメントを参照してください。 開発環境へのデプロイをフックにステージング環境のサーバーにもデプロイ トモニテでは、開発プロセスの効率化と品質向上を目指し、ビルドに AWS CodeBuild を利用しています。ステージング環境のサーバーへのデプロイは、開発ブランチへのマージをトリガーとして自動的に行う仕組みを構築しました。 これにより、開発者はコードをマージするだけで、ステージング環境に最新のビルドをデプロイすることができます。 以下は、AWS CodeBuild で使用する buildspec.yml の設定です。このファイルは、ビルドプロセスの各フェーズで実行されるコマンドを定義しています。 version : 0.2 env : variables : REPOSITORY_URI_BASE : <REPOSITORY_URI_BASE> DOCKER_BUILDKIT : "1" phases : install : commands : - GO_VERSION=$(cat .go-version) # Go のバージョンを取得 - REPOSITORY_URI=${AWS_ACCOUNT_ID}${REPOSITORY_URI_BASE}stg-server # Docker イメージのリポジトリ URI を設定 - TAG=stg - | # ecs-deploy ツールをダウンロードし実行可能にする echo "Setup ecs-deploy" curl -sL https://github.com/silinternational/ecs-deploy/archive/3.10.7.tar.gz | tar zxvf - mv ecs-deploy-3.10.7 ecs-deploy chmod +x ecs-deploy/ecs-deploy pre_build : commands : - docker login -u <USER_NAME> -p ${DOCKER_HUB_PASS} # Docker Hub にログイン - DATE=`date +%s` # 現在の日時を取得(イメージのタグに使用) build : commands : # Docker イメージをビルドしタグ付け - echo Building the Docker image... - docker build -f ./Dockerfile --build-arg GO_VERSION=$GO_VERSION -t $REPOSITORY_URI:$TAG . - docker tag $REPOSITORY_URI:${TAG} "${REPOSITORY_URI}:${TAG}.${DATE}" post_build : commands : - echo Logging in to Amazon ECR ... - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}${REPOSITORY_URI_BASE # Amazon ECR にログイン - echo Pushing the Docker images... - docker push $REPOSITORY_URI:$TAG # ビルドした Docker イメージをプッシュ - docker push "${REPOSITORY_URI}:${TAG}.${DATE}" - | # ECS クラスターに新しいタスクをデプロイ echo "deploy start" ecs-deploy/ecs-deploy --cluster <CLUSTER_NAME> \ --task-definition-file <タスク定義>.json \ --service-name <SERVICE_NAME> \ --region <region指定> \ --timeout 600 \ --image ${AWS_ACCOUNT_ID}.${REPOSITORY_URI_BASE}/<FAMLIY>:${TAG} cache : paths : - $GOPATH まとめ 今回の記事では、ステージング環境の構築について説明しました。本番データに接続し実際の運用を想定したテストを行うことで、リリース前に潜在的な問題を早期に発見し、サービスの品質を向上させることを目的としています。 ステージング環境の作成により考慮漏れによるリスクを軽減できたと考えています。 一方、この環境は比較的ライトに構築したため、さらなる改良の余地があると考えています。今回作成したものをゴールとせず、より良い構成になるよう今後も改善を重ねていきたいと思います!
Cloud SQL for MySQL 5.7 から 8.0 移行計画 はじめに こんにちは、TIMELINE 開発部 Service Development をしている ほんだ です! つい最近 Aurora MySQL バージョン 3 対応したな...。というのはさておき。 今回は Cloud SQL for MySQL のデータベースバージョンを 5.7 から 8.0 に移行する際に行った方法について紹介します! 前提 皆さんご存知かと思いますが、2025年2月1日からCloud SQLではMySQL 5.7, 5.6の拡張サポートが開始し、2028年2月1日はサポートが終了します。そこでTIMELINE が提供しているサービスの一つで、Cloud SQL for MySQL 5.7を使用しているため8.0への移行を行うことになりました。 今回のシステムでは月に1回程度メンテナンスでサービスが停止するため、そのタイミングでインプレイスアップグレードを行うことにしました。以下では5.7から8.0へ移行する際の事前準備、インプレイスアップグレードの手順、切り戻し方法について説明します。 事前準備 5.7と8.0の差分を確認 MySQLの公式 を参考に8.0での差分を確認しました。 今回はデフォルト認証プラグインが caching_sha2_password , character_set_server のデフォルト値が utf8mb4 に変わったことや collation_server のデフォルト値が utf8mb4_0900_ai_ci に変更されたことが差分として上がりました。 Cloud SQLの設定の確認 Cloud SQLはデータベースフラグというものを用いてMySQLパラメータの調整を行っているので、その確認も必要です。データベースフラグのデフォルトやどの値が設定できるかは こちら のサイトで確認ができます。 今回のデータベースでは上述5.7と8.0の差分で上がった default_authentication_plugin , character_set_server と collation_server をデータベースフラグを用いて設定することになりました。 手順 手順は下記の通りになります。 順番 手順 1 インスタンスのクローン 2 インプレイスアップグレード 3 MySQL versionの確認 4 データベースフラグの設定 ここからは実際に実行するコマンドも踏まえて説明していきます。 1. インスタンスのクローン インプレイスアップグレードを実行する際に、バックアップが作成されますが、こちらはあくまでデータのみのため別途手動でインスタンスのクローンを行います。 Cloud SQLインスタンスの名前は変更することはできないので、アップグレードに失敗した際に切り替えて問題ない名前にしとく事をお勧めします。 $ gcloud sql instances clone < SOURCE_INSTANCE_NAME > < DESTINATION_INSTANCE_NAME > --project =< PROJECT_ID > 2. インプレイスアップグレード 下記コマンドを用いて既存のインスタンスに対してインプレイスアップグレードを実行します。上述のように、インプレイスアップグレード開始時と終了時にデータのバックアップが作成されます。 今回は8.0.33にアップグレードするため database-version に MYSQL_8_0_33 を指定します。 $ gcloud sql instances patch < INSTANCE_NAME > --database-version = MYSQL_8_0_33 インスタンスのバックアップについては下記コマンドで確認することができます。 $ gcloud sql backups list --instance =< INSTANCE_NAME > インスタンスのアップグレード状況は下記コマンドで確認することができます。 $ gcloud sql operations list --instance =< INSTANCE_NAME > $ gcloud sql operations describe OPERATION 3. MySQL versionの確認 Cloud SQL Auth Proxyを用いてCloud SQLに接続しMySQLのversionを確認します。8.0系になっていることが確認できたらOKです。 $ cloud-sql-proxy --port < PORT_NUMBER > < INSTANCE_CONNECTION_NAME > $ mysql -u < USER_NAME > 127 . 0 . 0 . 1 -P < PORT_NUMBER > -p mysql > select version () ; 4. データベースフラグの設定 上述の通り、MySQLのdefault設定と異なる部分があるので、Cloud SQLのデータベースフラグを更新を行います。 $ gcloud sql instances describe < INSTANCE_NAME > $ gcloud sql instances patch < INSTANCE_NAME > --database-flags = character_set_server =utf8, collation_server =utf8_general_ci, default_authentication_plugin =mysql_native_password 下記コマンドにてデータベースフラグが更新されていることを確認します。 $ gcloud sql instances describe < INSTANCE_NAME > 念の為インスタンスに接続しMySQL commandでも確認すると良いです。 mysql> show variables like '%char%' ; 切り戻し方法 インスタンス名が変わっていることに注意して、手順1. で作成した既存のインスタンのクローンにそれぞれのサーバーの向き先を変更します。 まとめ 以前Aurora3への移行を行った際はconsoleベースで、アップグレードを行ったのですが今回gcloud commandを用いて行うことで、以前 rymiyamotoの記事 でもあった通りメンテナンスの再現性や、レビューが格段に容易になったと感じました。 また、今回の手順に記したものを実際の値に変えた手順書を作成しているため、当日はコピペするだけで作業が完了するので実行者の負荷やヒューマンエラーも軽減することができました!(筆者はタイポが多いです) 今回Cloud SQLのアップグレードを行うにあたって、明確な原因は追求できていませんがsidecar containerとして実行している、Cloud SQL Auth Proxyのimage versionが古すぎて、インスタンスに接続できないという事象が発生したので、定期的にメンテナンスを行うことは大切だと実感しました。 Cloud SQL for MySQLのインプレイスアップグレードを行おうとしてる人や今後本システムの開発に携わる人の参考になれば幸いです。
タイトル - リテールハブ開発部の新設とDELISHKITCHEN開発部長の交代 はじめに 皆さん、いつもお世話になっております。CTO/開発本部の今井です。 本ブログでは、あたらくリテールハブ開発部を立ち上げたことおよび自分が兼務で務めていたDELISHKITCHEN開発部長の交代についてお話させていただきます。 リテールハブ開発部の設立 このたび、10/1でリテールハブ開発部を新設し、そこの開発部長を私が務めることになりました。 リテールハブ開発部は、これまでDELISHKITCHEN開発部で手掛けてきた小売向けのサービス開発に注力をする新しい部門となります。 先んじて、ビジネス側はすでにリテールハブカンパニーとして切り出されていたところに合わせて、開発部も切り出すことを決断しました。 私たちの会社は、これまで多岐にわたる分野でサービスを提供してきましたが、小売業界は特に大きな成長が期待される分野の一つです。 お客様のニーズが日々変化する中で、より迅速で柔軟な対応が求められるようになりました。 そこで小売業界向けのサービス開発にさらに集中し、その分野におけるリーダーシップを強化するため、リテールハブ開発部を新たに立ち上げました。 組織図 この新しい部門では、既存の「ストアDX」「ネットスーパー」「小売アプリ」の開発を推進し、 『retail HUB』として統合ソリューションの提供を行うとともに、リテールメディアの構築を行っていくことに注力します。 私自身も、これまで培ってきた経験を最大限に活かし、チームと共により良いサービスを提供していけるよう努力していきたいと思っています。 新任DELISHKITCHEN開発部長の紹介 私がこれまで担当してきたDELISHKITCHEN開発部の新しい部長には、村上が就任します。 村上は、新卒で弊社に入社して以来、驚異的なスピードで成長を遂げてきた期待のエンジニアです。 彼の優れた技術力とリーダーシップは、すでに多くのプロジェクトで発揮されており、私たちのチームにとってなくてはならない存在です。 短期間で当時MAMADAYS開発部(現在のトモニテ開発部)のサーバーサイドのマネージャーに就任したのちに、DELISHKITCHEN開発部の副部長として主に広告商品の開発を一手に引き受けるなど大きな役割を果たしてきました。 彼の強みは、単なる技術力だけでなく、チームを引っ張るリーダーシップと、メンバーの意見を尊重しながらも、全体の方向性を見極めて決断する判断力にあります。 DELISHKITCHEN開発部は、新たなリーダーのもとで、さらに力強いチームへと進化していってくれると確信しています。 開発部長を彼に引き継げることは、私自身とても嬉しく思っています。 DELISHKITCHEN開発部の今後 今後村上が率いることになるDELISHKICHEN開発部は、これからも私たちの会社にとって軸となるサービスを運営・発展させるという重要な役割を担い続けます。 特に、小売向けのサービス開発をリテールハブ開発部に引き継いだことで、AIの活用をはじめとし、よりチャレンジングな領域でのイノベーションを生み出すことが期待されています。 村上のリーダーシップのもと、DELISHKITCHEN開発部は新たな成長を遂げ、多くのお客様に価値あるサービスを提供していけるよう邁進していきます。 また、組織は別れたものの、DELISHKITCHENとリテールハブは、それぞれの開発部が独立しているわけではなく、 システム連携や、リソースの共有や協力を行うことでより良いサービスを提供することができると考えています。 両部門の緊密な連携により、相乗効果を生み出し、「食」の領域をリードしていくことを目指していきます。 最後に 今回の新しい開発部の設立および部長交代を通じて、私自身も新たなステージで挑戦を続けていく決意を新たにしています。 リテールハブ開発部での取り組みを通じて、小売業界に革新をもたらし、より多くのお客様に価値あるサービスを届けていけるよう尽力していきます! また、どちらの部門も進めたいことが増えていく中で、一緒に取り組んでいける仲間がまだまだ足りません! ぜひ一緒に日本の「食」の領域をリードするサービスを作っていきましょう! corp.every.tv
はじめに こんにちは。DELISH KITCHEN 開発部 RHRA グループ所属の池です。 2024年6月、エブリーは5つの小売アプリの運営について事業譲渡を受け、『 retail HUB 』へ移管しました。 prtimes.jp この事業譲渡において、私はシステムに関するデューデリジェンス(以下、システムDD)を担当しました。 今回 retail HUB へ移管したシステムは具体的には 5つプロダクトそれぞれにおける、iOS/Androidのネイティブアプリと、入稿管理画面の Web アプリケーションサーバー、アプリ向け API サーバー、それらを構成するシステム(AWS環境、ロードバランサー、データベース、バッチサーバーなど)です。 システムDDは譲渡されるIT資産の把握やリスクの確認を行う重要なプロセスですが、方法や手順が明確に決まっているようなものではなく、どう進めるべきか迷うことが多くありました。 そこで、本記事では、今回私が行ったシステムDDの具体的な手順の実例について紹介します。進め方に明確な正解のないシステムDDについて、手順の実例を共有することで、同様のプロセスを進める方々の参考になれば幸いです。 システムデューデリジェンスとは システムデューデリジェンスとは、事業譲渡やM&Aの際に、譲渡対象となるシステムやIT資産の現状を詳細に調査し、リスクを特定するプロセスです。主に、譲渡後に予期しない障害やコストの増加を防ぐために実施されます。 システムDDの手順実例 私が行ったシステムDDの手順を振り返ると、以下の手順に整理できました。 事前準備 前提・目的の認識合わせ 調査範囲の明確化 調査 システムの満たしたい状態の定義 ドキュメントの満たしたい状態の定義 システム評価 調査項目の洗い出しとグルーピング アプリケーション(iOS/Android/サーバー) インフラストラクチャ(AWS) 現場エンジニアへのヒアリング 調査後 見つかった課題・リスクにおける対応の整理 ここからはそれぞれの手順について詳細を紹介します。 事前準備 前提・目的の認識合わせ システムDDを進めるにあたり、まず最初に取り組んだことは事業譲渡における前提や目的の認識合わせです。 システムDDは社内でも初めても取り組みでもあることに加え、デューデリジェンスは実際にはその時の事業状況や、事業譲渡の目的などによって具体的な調査範囲・内容は変わり得るため、認識を合わせる必要があると考えました。認識合わせは当たり前なことですが忘れてはいけない大切な作業だと考えています。 今回の場合、事業譲渡がプロダクト単位で複数のプロダクトが譲渡対象であることや、各プロダクトの所有権や権利等の状況が異なる状況であること、のようなことが前提となります。 そして、主な目的は、譲渡対象となるIT資産の現状を把握し重大なリスクを特定することです。特に、譲渡後の運用が想定通りの工数で行えるか、また運用に影響を与える潜在的な課題やリスクを洗い出すことが重要でした。さらに、前提を踏まえると譲渡対象のソフトウェア資産を特定することも大事な目的になります。 調査範囲の明確化 以上の前提と目的を踏まえ、以下の点が重要となります。 プロダクト固有のリスクの評価 運用に要する工数の把握、および運用に影響するリスクの特定 譲渡対象のソフトウェア資産の洗い出しと特定 これらの点を踏まえて調査範囲を明確化し、システムDDを行いました。 調査 ここからは具体的に行った調査方法について紹介します。 システムの満たしたい状態の定義 運用保守への影響確認という前提の観点を踏まえて、譲渡後にエブリーが運用保守していくあたり、困難なく開発・保守・運用できるかどうかという観点で、システムの満たしたい理想状態を定義し、その状態との差分を確認していくことで調査および評価を行いました。 例えば、「バグ検知」という調査の大項目については以下のように定義しました。 エラー検知の仕組みが導入されていてバグやシステム異常の発生が即時に検知・通知されること 各システムの各種メトリクス監視 アプリのクラッシュ計測 など... このような定義との差分をドキュメントや、実際のソースコードおよびシステム構成の確認、現場エンジニアへのヒアリングなどを通して確認していきました。 また、満たしたい状態を定義することで、調査の際に確認すべきポイントが明確になります。 システム状態定義のイメージ ドキュメントの満たしたい状態の定義 システムの状態定義と同様に、ドキュメントについても運用保守という観点を踏まえて期待する状態を定義し、その状態との差分を確認していくことで調査および評価を行いました。 例えば、「DB設計書・ER図」というドキュメントについては以下のように定義しました。 下記の内容を把握でき、結果としてDBの運用・改修が可能な状態 各物理データベース 用途 スペック DBMS ソフトウェアバージョン 各種メトリクスの閲覧方法 異常時の検知手法・対応手法 各論理データベース 各テーブルの構成、ER 図 各テーブルやビュー・ストアド等の用途 各テーブルやビュー内のカラムの定義 以下のようなドキュメントについて、同様の状態定義を行い、評価していきました。 外部システム連携仕様書 機能一覧 要件定義署 機能仕様書 API設計書 DB設計書・ER図 ログ設計書 各種アカウント情報 環境構築手順書 リリース手順書 管理画面操作手順書 画面一覧・画面遷移図 デザイン バッチ処理概要 レポート作成マニュアル など... ドキュメント状態定義のイメージ システム評価 システム評価では、上記の満たしたい状態を踏まえつつ、調査項目を洗い出してグルーピング行い、それぞれの調査項目についてGitHubやAWS環境などに招待いただいて閲覧操作することで評価を行いました。 アプリケーション(iOS/Android/サーバーなど) アプリケーションの評価は、以下のグルーピング項目に基づいておよそ40項目ほどの調査項目を確認していきました。 確認対象のリポジトリは約30個ほどあり、膨大な作業量であったため、現場エンジニアへのヒアリングを交えながら効率的に調査を進めました。 評価のグルーピング項目 ソースコード品質 設計 プロセス セキュリティ パフォーマンス 構成要素 具体的な調査項目(一部抜粋) ソースコード品質 コードベースの長さ コードベースの複雑さ テストコードのカバレッジ エラーハンドリング 適切なコメントの有無 一定期間内のエラー件数 既知の不具合の数 コーディング規約の有無 言語/FW/ライブラリのバージョン など... 可能であれば、全てのソースコードを実際に動作させて運用保守への影響を確認することが望ましいです。 アプリケーション評価項目のイメージ インフラストラクチャ(AWS) インフラストラクチャの評価は、AWS環境に関する以下のグルーピング項目に基づいて行いました。AWSアカウントに招待していただき、各項目に沿って確認しました。 アプリケーション評価と同様、現場エンジニアへのヒアリングを交えながら調査を進めました。 評価のグルーピング項目 アーキテクチャ設計 セキュリティ コスト管理 運用・保守 具体的な調査項目(一部抜粋) アーキテクチャ設計 VPC設計 ネットワーク構成 データベース設計 コンテナ化の有無 IaCの有無 CI/CD構成 ログ収集とモニタリング セキュリティ IAMポリシーとロールの設定 セキュリティグループとネットワークACLの設定 データの暗号化 ログ管理と監視 現場エンジニアへのヒアリング 上述の調査での不明点や把握しきれない点について、現場エンジニアの方々からヒアリングを行いました。 現場のエンジニアの方々は、システムの現状をよく理解しているため、ヒアリングを通して価値のある情報を多く早く得ることができます。 ヒアリングは必須の作業として行うと望ましいです。できればシステムDDの早い段階で行い、かつ定期的に行うことで効率的に調査を進めることができると思います。 見つかった課題・リスクにおける対応の整理 以上の調査を行った結果、見つかった課題・リスクを一覧化し、それぞれに対して影響範囲を整理しました。 その内容をもとに先方と対応方針について協議を行いました。 やっておけばよかったこと 譲渡後に運用保守を始めから振り返ってみて、システムDDでいくつかやっておけばよかったと感じたことがありました。主に運用してみて想定外の工数に繋がったものです。 システムの移管作業を想定した調査観点の追加 既知の不具合に対する詳細と影響範囲の把握 など... 譲渡契約後にシステムを当社に移管する作業を行う進め方をしましたが、実際に移管作業を始めると、移管するための事前作業で多くの工数を要するものがあることがわかりました。 例えば、iOSアプリの移管について、キーチェーンやAppleでサインイン機能を利用しているiOSアプリを別組織に移管すると、それらの機能が一時的に利用できなくなることがわかりました。これらをユーザー影響なく移管するには多くの工数を要するため、譲渡時には想定していなかった想定外の工数となります。 また、把握していた既知の不具合が譲渡後に想定外の影響を及ぼし、改修するのに多くの工数を使ったケースがありました。事前に不具合の詳細と影響範囲の把握を行っていれば、多少は事前に織り込めたと思います。 まとめ 今回の経験を通じて、事業への影響を最小限に抑えるために事前にシステムDDを行うことの重要さを再認識したとともに、観点やノウハウなど知見を貯めることができました。 また、5つのプロダクトを同時に移管するという稀有な経験から、私自身の成長として、未知で決まった答えのない領域に対して試行錯誤しつつ最大限の決定を繰り返して推進していく能力が向上したと感じました。 本記事が少しでもどなたかのお役に立てれば幸いです。
はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 この度、エブリーは 2024 年 10 月 19 日(土)に開催される『Vue Fes Japan 2024』に、ゴールドスポンサーとして協賛することになりました! vuefes.jp エブリーでは、DELISH KITCHEN を現在 Nuxt.js(Vue.js)で構築しており、2018 年から採用しています。 今回の協賛を通して、さらなる Vue.js コミュニティの発展に貢献できればと考えております。 近年は Vue.js 周辺のエコシステムでは新たな潮流も生まれつつあり情報交換や交流を通じて、新たな出会いや気づきを得ることができるでしょう。 ぜひ、タイムテーブルをご覧いただき、気になるセッションに参加してみてください。 vuefes.jp エブリーにおける Nuxt.js(Vue.js) の活用 Nuxt.js 導入の背景 エブリーでは、DELISH KITCHEN の Web を最初 Riot.js という SPA ライブラリと クローラー向けには静的な HTML を返すため、Express を使って SSR を行っていました。 この構成上コードが二重管理されており、新規ページや機能の開発、運用コストが大きくなっていました。 このような課題を解決するために、Universal アプリケーション(SSR + SPA)の開発が可能な技術を検討し、Angular Universal、Next.js、Nuxt.js の中から Nuxt.js を選択しました。 選定の決め手は以下の通りです。 バックエンドに強いチーム構成で、Web フロント開発に長けたメンバーが少なかったため、Nuxt.js のような薄いフレームワークが取り掛かりやすいと考えた。 チーム内に Vue.js の開発経験者がいた。 Riot.js の SFC(Single File Component) の構造が Vue.js のコンポーネントと似ていたため、システムリプレースが容易だった。 移行後のメリットとして、ユーザー向けとクローラー向けのコードの二重管理がなくなり、一元管理が可能になりました。 これにより開発や QA のコスト削減が実現され、Nuxt.js(Vue.js)の豊富なドキュメント、ライブラリ、活発なコミュニティの恩恵を受け、開発の問題解決が容易になりました。 詳細については、以下の記事をご参照ください。 tech.every.tv 他にもエブリーのテックブログでは、Vue.js に関する記事を随時公開しています。 Nuxt3 化に向けた取り組み 2018 年から Nuxt.js を採用してきたエブリーでは、現在 Nuxt3 化に向けた取り組みを進めています。 こちらも合わせてご覧ください。 tech.every.tv tech.every.tv tech.every.tv Vue.js の開発効率化 Vue.js を活用して開発開発効率化を図るために、VueUse を扱った内容も公開しています。 tech.every.tv tech.every.tv 皆様とお会いできることを楽しみにしています! 私たちのブースでは、Vue の活用事例等をご紹介する予定です。 またブースには素敵なノベルティもご用意しております。詳細はまた追ってお知らせいたしますので、ぜひ、お気軽にお立ち寄りください! Pre Party を共同開催します! エブリーでは、Vue Fes Japan 2024 を盛り上げるべく、同じくスポンサーである OPTiM さんと共同で Pre Party を開催します! optim.connpass.com LT(ライトニングトーク)が主体のカジュアルなイベントとなっております。 募集枠は全部で 4 つありますので、ぜひ奮ってご応募ください! Vue Fes Japan 2024 の参加予定の方や、惜しくも参加できない方も、ぜひご参加ください! こんな方におすすめです! Vue Fes Japan 2024 に向けて、知り合いを増やしたい方 Vue Fes Japan 2024 のプロポーザルがリジェクトされた、敷居が高いと感じた方で LT で発表してみたいなと思っている方 Vue に興味があるエンジニアや学生の方 Vue での開発に従事されている方 OPTiM、エブリー がどのように Vue を活用した開発をしているのか興味がある方 OPTiM、エブリー のエンジニア組織について興味がある方 Vue を中心に知識と交流の輪を広げましょう! ※本勉強会/LT会はVue Fes Japan公式のものではなく、スポンサー同士の有志のイベントとなります。そのため本勉強会へのお問い合わせをVue Fes Japan様へ行うことはご遠慮ください。 エブリーでは、ともに働く仲間を募集しています。 エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv X ではテックブログや登壇情報、インタビュー記事などエブリーのエンジニアに関する情報を発信していますので、ぜひご覧ください。 https://x.com/every_engineer 最後までお読みいただき、ありがとうございました!
はじめに こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している kikuchi です。 昨今 AI がますます普及し業務で AI を活用する事例も増えてきましたが、Google が提供している Gemini が Android Studio の一機能として提供されていることをご存知でしょうか? 2024/9/13 時点ではまだプレビュー版ではありますが無料で公開されていますので、今回は実際に使用した際の環境構築手順、使用感などをまとめてみたいと思います。 Gemini の詳細は今回割愛しますが、Gemini は大きく分けて無料版の Gemini、有料版の Gemini Advanced と分かれており、現時点では Android Studio は無料版の Gemini と連動しているようです。 事前準備 Android Studio と Gemini を連携する場合は Google アカウントが必須となりますので、事前に作成をしておいてください。 なお、プレビュー版では Gemini Advanced への加入状況は反映されないため、Gemini Advanced への加入は不要です。 環境構築手順 公式サイト から Canary 版 (2024/9/13 時点では Ladybug) をダウンロードし、Android Studio を起動します プロジェクトを生成後、右上にある Gemini のアイコンをクリックします 「Log in to Google」をクリックするとブラウザが起動するためブラウザで認証を通し、完了後に「Next」をクリックします プライバシーポリシーを確認し、「Next」をクリックします 利用規約を確認し、チェックを付けて「Next」をクリックします プライバシー設定を確認し、任意の項目にチェックを付けて「Finish」をクリックします 以上で Gemini の機能を使用できる準備が完了しました。 操作方法について 操作方法はコメント入力欄に質問内容を入力し 「Submit」 をクリックすると AI が回答をしてくれるという形で、通常のブラウザ上で操作するものと大きな違いはありません。 以下は 「ログイン画面のソースファイルとレイアウトファイルを作成して」 と質問した時の回答です。 AI のサービスを使用したことがある方は違和感なく使用できるかと思います。 ファイル操作 Android Studio の Gemini では通常のブラウザ上で使用するようなチャット形式でやり取りが出来る点に加え、生成したソースコードに対して特殊な操作が行えるようになります。 上記は実際に Android Studio 上の Gemini で生成したソースコードの直下に表示されるアイコンですが、左から順に Copy → ソースコードをコピーする Insert at Cursor → カーソルが当たっている箇所に該当のソースを挿入する Insert in New Kotlin File → ソースコードを新規ファイルとして生成する (レイアウトファイルの場合はレイアウトファイルとして新規で生成する) Explore in Playground → 生成されたコードを Playground 上で確認する といった操作が行えます。 1、2、4 についてはソースコードのコピーやコピー&ペーストを一括で実施してくれる機能となりますが、3 についてもう少し細かく動きを見てみたいと思います。 Insert in New Kotlin File の実施例 実際に回答結果で出力されたソースファイルの「Insert in New Kotlin File」を実行してみたいと思います。 ファイルを追加したいディレクトリを選択した状態で「Insert in New Kotlin File」のアイコンをクリックします。 実行後、ファイルが自動的に追加された上、 androidx.appcompat:appcompat:1.7.0 のライブラリが不足していることを検知し、追加するかの提案もしてくれます。 「Add」 をクリックすると build.gradle に追加してくれるだけでなく、Version Catalog でライブラリを管理している場合は libs.versions.toml にも変更を加えてくれます。 1 つ注意点としては、AndroidManifest.xml には自動的に定義が追加されないため、手動で activity の定義を追加する必要があります。 1 つ手動操作が必要なものの、ソースコードはファイル生成からライブラリの導入までスムーズに行うことが出来ました。 では引き続きレイアウトファイルの生成を行ってみます。 レイアウトファイルの場合はアイコンの説明が「Insert in New Layout File」になるため、そちらのアイコンをクリックします。 レイアウトファイルの場合はここで 2 つほど問題が発生してしまいます。追加後の状態は以下の画像のようになります。 問題の 1 つ目、どのディレクトリを選択していても res ディレクトリ直下に「new.xml」というファイル名で生成されてしまうため、手動でリネームとパス移動が必要になります。 そして 2 つ目、自動生成されたレイアウトファイルは ConstraintLayout を使用していますが、このプロジェクトでは androidx.constraintlayout:constraintlayout のライブラリを設定しておらず、 こちらはソースコードの場合と違ってライブラリが不足していることを検知しませんでした。 レイアウトファイルの自動生成については、手動操作、エラーの解析、ライブラリの追加が必要となってしまいます。 本項でファイルの自動生成を試してみましたが、ソースファイルの生成は容易なものの、レイアウトファイルの生成にはまだまだ課題がありました。 コード補完 前項ではチャット形式でのやり取りについて説明しましたが、実際のソースコードのドキュメント作成や補完を行えるコード補完の機能についても触れたいと思います。 先ほど自動生成したファイルのメソッドで右クリックをすると以下の機能を選択できます。 こちらは上から順に Document Function "<メソッド名>" → メソッドの KDoc を作成する Comment Code → 処理にコメントを付ける Explain Code → 処理の解説をする Suggest Improvements → 処理の改善案を提示する Rethink variable names → 無反応のため不明 (直訳だと変数名の再定義) となっており、どれも該当のソースコードに特殊なプレフィックスを付けてチャット欄に貼り付けるものでした。 最終的には通常のチャット形式でのやり取りになるため、3 のみ試してみたいと思います。 Explain Code の実施例 実際にメソッドの部分で Explain Code を選択すると以下の警告が表示されます。 解析のために Gemini のサーバーにソースコードを送信してよいかという質問になります。機密情報を含むソースコードの取り扱いには注意が必要なため、適切な判断をする必要があります。 今回は先程 Gemini で自動生成したソースコードのため、そのまま送信をしてみます。 プレフィックスとして Explain the following code: というものが付いて、後は丸々ソースコードがチャット欄に貼り付けられる、という挙動になりました。 あとは「submit」をクリックします。 ソースコードの解析結果がチャット欄に出力されました。 こちらはチャット欄で「コードを解析して」といった文章を入力したり、ソースコードをコピー&ペーストする手間を省く効果が期待できそうです。 注意点 おそらく 1 時間に 10 回程度質問のやり取りをしたところで上限に達してしまいました。1 時間程度待てば再度使用できるようになりましたが、ここは現時点で無料版を使用しているための制限になるかと思います。 実際の使用例 色々と試した中で一番便利に感じたのは、ファイル操作の「Insert at Cursor (カーソルの位置に自動挿入)」と「Insert in New Kotlin File (ファイル自動生成)」を組み合わせて使用する形でした。 具体的な例を記載してみます。 「ログを出力する Utility クラスを作成して」と質問し、出力結果で「Insert in New Kotlin File」を実行しファイルを自動生成する 「verbose のログを出力するメソッドを作成して」と質問し、出力結果で「Insert at Cursor」を実行しクラスを拡張する 手動で実装することなくチャットによるやり取りのみでクラスの生成、クラスの拡張を行うことが出来ました。 ビジネスロジックを含むようなクラスの生成は難しいかもしれませんが、Utility クラスや data クラスなど単純なメソッドの組み合わせになるようなクラスであればこちらの方が速く実装できる可能性があります。 まとめ 今回 Android Studio の Gemini 機能を使用してみましたが、ブラウザと Android Studio を行き来する手間もなくなり、ファイルの自動生成など Android Studio の操作の手間をかなり軽減し、 また最低限の知識さえあればソースコードをほぼ書かずに実行ができる状態まで作れるため、開発のサポートとしては非常に強力だと感じました。 ただし、前述の通りまだプレビュー版のため、無料版の Gemini を使用していることで回答が返るまでに 10 秒程度かかってしまう点、レイアウトファイルの操作は完全に自動化されていないなど、 まだまだ課題も目立っています。 今後どう改修されていくかは分かりませんが、結局は Android 開発の正しい知識は必須であることには変わりないため、あくまで開発の補助に留めて使用するのがベストかと感じました。 今回紹介した内容が少しでも皆さまのお役に立てれば幸いです。
【2024最新】AWS Data Firehoseを使った際の4つの問題とその解決策 背景 こんにちは、開発本部 DELISH KITCHEN Retail HUB NetSuperグループに所属するフルスタックエンジニアをやらせていただいています、ホーク🦅アイ👁️です。2024/2/9、 Amazon Kinesis Data Firehose から Amazon Data Firehose に名称変更されてから半年ほど経過しておりますが最新の設定情報などが公開されていることが少ないと感じたため今回記事を書くに至りました。 前提 今回の記事を書くにあたっての前提条件は以下のようになっております。 ECS Fargate上にWEBアプリケーションコンテナが存在し、そのコンテナは標準出力にアクセスログを出力している アクセスログは、JSON形式ログとスペース区切り形式ログが混在している # JSON形式ログ例 {"message": "access", "key": "value"} # スペース区切り形式ログ例 127.0.0.1 - - [09/Sep/2024:01:54:49 +0000] "GET /healthcheck HTTP/1.1" 200 236 "-" "curl/7.74.0" TaskDefinitionで logDriver を awslogs に設定しその標準出力はCloudWatch Logsに常に転送されている 主な作業を調査・実施した年月日は2024年7月12日 要件 要件1 WEBアプリケーションログはJSON形式ログとスペース区切り形式ログが混在しているのでそれぞれを別のS3プレフィックスに保存したい 要件2 JSON形式ログは{unique_code}/日付でパーティションをして1行ずつ改行されるようにS3保存させたい スペース区切り形式ログはノンパーティションで1行ずつ改行されるようにS3保存させたい 但し、これらは自前のLambdaを使わずに行うこと! 実装手段 要件1に対する実装 CloudWatch Logsのサブスクリプションフィルターを使うとログの種類を2つに分けることができます。 コンソール上にも注意書きがありますがサブスクリプションフィルターはLog Groupごとに2個までしか設定できないので注意してください。以下、Terraformで3つ目を適用した時のエラーです。 Error: putting CloudWatch Logs Subscription Filter (XXXX): operation error CloudWatch Logs: PutSubscriptionFilter, exceeded maximum number of attempts, 25, https response error StatusCode: 400, RequestID: XXXX, LimitExceededException: Resource limit exceeded. Terraformを使って反映 # pointだけ絞ってピックアップ resource "aws_cloudwatch_log_subscription_filter" "application_log" { name = "application-log" role_arn = var.subscription_filter_webserver_arn log_group_name = aws_cloudwatch_log_group.webserver.name destination_arn = var.kinesis_firehose_stream_applicationlog_arn filter_pattern = "{$.message = \"access\"}" } resource "aws_cloudwatch_log_subscription_filter" "access_log" { name = "access-log" role_arn = var.subscription_filter_webserver_arn log_group_name = aws_cloudwatch_log_group.webserver.name destination_arn = var.kinesis_firehose_stream_accesslog_arn filter_pattern = "\" \"" } スペース区切り形式ログのfilter_patternは半角スペース1文字で可能でTerraformで記述するときはダブルクォーテーションで括る必要があります。 要件2に対する実装 Terraformを使って反映 # pointだけ絞ってピックアップ dynamic_partitioning_configuration { enabled = "true" } processing_configuration { enabled = "true" processors { type = "RecordDeAggregation" parameters { parameter_name = "SubRecordType" parameter_value = "JSON" } } processors { type = "MetadataExtraction" parameters { parameter_name = "JsonParsingEngine" parameter_value = "JQ-1.6" } parameters { parameter_name = "MetadataExtractionQuery" parameter_value = "if .context.unique_code then {unique_code: .context.unique_code} else {unique_code: \"NONE\"} end" } } processors { type = "Decompression" parameters { parameter_name = "CompressionFormat" parameter_value = "GZIP" } } processors { type = "CloudWatchLogProcessing" parameters { parameter_name = "DataMessageExtraction" parameter_value = "true" } } processors { type = "AppendDelimiterToRecord" } } JSON形式ログの方は、Dynamic Partitioning(動的パーティショニング)を使えばJSONパースが行われてS3のプレフィックスにパーティションされます。この際、1ログごとに付与される改行コードがなくなってしまいます。そこで改めてDestination(送信先)データに改行コードを付与するためにProcessorsのAppendDelimiterToRecord typeを設定する必要があります。Terraformで適用するとコンソール上では「 New line delimiter(改行の区切り文字) 」がenabled(有効)になります。一方で、スペース区切り形式ログの方はDynamic Partitioningを使う必要がないのでTransform and convert records(レコードを変換および転換)機能を行うだけで改行コードが付与されているので改めてAppendDelimiterToRecordを設定する必要はありません。 問題 問題1 Dynamic PartitioningのJSONパースにおいて指定したパーティションキーにNULL値があるときパーティション作成に失敗していた "errorCode":"DynamicPartitioning.MetadataExtractionFailed","errorMessage":"partitionKeys values must not be null or empty" 問題2 Firehose自体のエラーログに以下のエラーメッセージが出てS3保存に失敗していた errorCode":"DynamicPartitioning.MetadataExtractionFailed","errorMessage":"Non UTF-8 record provided. Only UTF-8 encoded data supported" 問題3 terraform applyを実行すると以下のエラーメッセージが出て適用に失敗した operation error Firehose: UpdateDestination, https response error StatusCode: 400, RequestID: d7b3d246-ecb3-a51f-88ba-1557ca6eae2a, InvalidArgumentException: Enabling source decompression is not supported for existing stream with no Lambda function attached. Terraform provider hashicorpのバージョンを5.44->5.58(当時の最新版)に上げるも変化なし 問題4 Athenaでクエリ発行するときに、0件ヒットになってしまう JSONコンテンツのみデータ取得できていない感じ 結論、S3に保存しているデータはGZIP圧縮されているが拡張子が.jsonなのでそれを認識できていないため解凍せずにそのまま読み込もうとしている様子 拡張子を.gzにしたら読み込めた プレーンテキスト状態の.jsonファイルと圧縮状態の.gzファイルを混在しても両方同時に読み込めていた 解決策 問題1に対する解決策 JQのif-then-else制御構文を使うことでNULL値を特定の固定文字列に置換してしまえば解決できました。以下のようにTerraformでparameter_value値に記述して適用します。尚、この値は長文なので実際にコンソール上で設定画面のDynamic partitioning keys表示を見ても文字列全文は出てこないですが正しい挙動になるので問題ありません。 parameter_value = "if .context.unique_code then {unique_code: .context.unique_code} else {unique_code: \"NONE\"} end" 問題2に対する解決策 原因調査をするにあたって、ChatGPTに質問してその情報を足がかりに進めることにしました。 質問スクリプト "errorCode":"Lambda.ProcessingFailedStatus","errorMessage":"ProcessingFailed status set for record" というエラーが出て先ほど教えたJSONデータログをutf-8変換に失敗しました。なぜでしょうか? 回答 ・Firehoseに渡す前にLambda関数でutf-8に変換しておく ChatGPTの回答を受けて「結局Lambdaを間に挟まないとダメなのか?!」と疑問視しつつ、該当するBluePrintのLambda関数をReadingするとそもそもCloudWatch LogsからFirehoseに渡る過程でサブスクリプションフィルターを利用するとデータ構造が生ログではなくなっていることが判明しました。つまり、Firehoseに渡るデータは何もしないと圧縮データかつJSON構造変更、そして元ログデータ自体がBASE64エンコード化していました。故に、解凍、パース、デコード処理が必要になるということでした。 { " messageType ": " DATA_MESSAGE ", " owner ": " 123456789012 ", " logGroup ": " log_group_name ", " logStream ": " log_stream_name ", " subscriptionFilters ": [ " subscription_filter_name " ] , " logEvents ": [ { " id ": " 01234567890123456789012345678901234567890123456789012345 ", " timestamp ": 1510109208016 , " message ": " log message 1 " } , { " id ": " 01234567890123456789012345678901234567890123456789012345 ", " timestamp ": 1510109208017 , " message ": " log message 2 " } ... ] } 参考) Transform source data in Amazon Data Firehose - Amazon Data Firehose 便利なサブスクリプションフィルターを使わない選択肢はないので、これを使いつつパースするFirehoseの新たな機能が2024年2月27日にリリースされたようでその機能を使うことで自前でLambda関数を用意せずにFirehoseの機能だけで解決させることができました。 Amazon Data Firehose に解凍された CloudWatch Logs のメッセージ抽出機能を追加 具体的には、コンソールで言うところの特定Data Firehoseのconfigurationページを開いて「Transform and convert records」→「Decompress source records from Amazon CloudWatch Logs」と「Extract message fields only from log events」をONにします。 ちなみに、 Firehose新機能の内部構造は結局Lambda関数を使っているようです(メッセージ抽出機能自体は追加料金はありませんがおそらくCloudWatch Logsからのソースレコード解凍機能部における追加料金はLambda関数料金分なのではないかと推測)。 Amazon Data Firehose Firehose streams Configuration 問題3に対する解決策 結論としては、Terraformでresourceのattributeであるprocessing_configuration内でDecompressionとCloudWatchLogProcessingの2つのtypeのみを設定すれば解決策2の設定が適用されます。しかし原因はよくわからないですがTerraformでDecompressionとCloudWatchLogProcessingを設定せずにFirehoseリソースを新規作成した後に設定変更という形で前述の2つのtype設定をTerraformで更新適用しようとするとエラーになった(plan時はエラーは出ない)ので途中で変更する場合は工夫が必要ということが判明しました。以下、具体的な工夫手順です。 まず適当に既存のLambda関数を用意した上でそのLambdaを使って変換処理を行う記述も追記して適用 その後、Lambda関数による変換記述をコメントアウトなどしてそのprocessors部分だけ削除更新するterraform applyを実施することが可能なのでそれを適用 processors { type = "Lambda" parameters { parameter_name = "LambdaArn" parameter_value = "arn:aws:lambda:$ { region } :$ { account_id } :function:$ { parse_func_name } :$LATEST" } } 問題4に対する解決策 FirehoseでS3に転送する時に再圧縮して保存させているのでそのファイルが圧縮されたファイルであることをAthenaに認識させるためにはContent-Typeではなく拡張子を.gzにすることであると判明したため、その設定をTerraformに施し適用します。 resource "aws_kinesis_firehose_delivery_stream" "app_server_log" { name = "app-server-log" destination = "extended_s3" extended_s3_configuration { bucket_arn = var.access_logs_arn role_arn = var.iam_role_kinesis_stream_app_server_arn buffering_interval = 300 buffering_size = 64 compression_format = "GZIP" custom_time_zone = "Asia/Tokyo" file_extension = ".json.gz" # <= この部分! ここで、元々の経緯は、圧縮しているにもかかわらず。.json拡張子にしていた理由としてChromeブラウザでS3コンソールからダウンロードを実行した時、自動で圧縮ファイルを認識して解凍してローカルディスクに保存する。その時に拡張子.gzのままだとファイルを開いたときにエラーが出て開けないのでファイル名変更で拡張子を手動で.gz部分を削除して.json拡張子にさせてから開かないといけないという手間が発生することに起因します。 総括 結論 Amazon Data Firehose解凍機能のみを使ってCloudWatch LogsのログメッセージをS3に転送すること自体は他の記事でもありました。しかし、本記事のようにJSON形式ログの変換を追ってTerraformを使ってまとめている記事はなかったように思います。 Amazon Data Firehose解凍機能を使うべき理由は、公式DOCによるLambdaで提供しているblueprints関数テンプレートが非推奨(deprecated)となっていることにあります。ただし、2024年9月執筆時点では、未だLambda関数を作成するコンソールで該当のblueprintsを選択および作成できます。 Dynamic Partitioningを使う際に、JQのif-then構文を駆使してパーティションキーを柔軟に設定できました。 Amazon Data Firehose解凍機能をTerraformで適用する場合、注意点があることがわかりました。 現状の課題 Amazon Data Firehose解凍機能が有償である点ですが、blueprintsのLambdaを設置した時の料金と比較しても大差がないくらい安い点で現状は採用しております。もし完全に無償でこの辺りを構築する場合は、そもそもECS FargateでFluentdなどの外部ログエージェントを使って直接S3に転送するアーキテクチャを採用することになると思います。 みなさまの快適なログライフを! 参考 改行系記事 https://dev.classmethod.jp/articles/amazon-kinesis-data-firehose-transform-source-records-with-aws-lambda/ https://dev.classmethod.jp/articles/kinesis-data-firehose-dynamic-partitioning-json-parse/ https://dev.classmethod.jp/articles/the-idea-of-automatically-inserting-newline-codes-between-records-without-using-a-lambda-processor-in-amazon-kinesis-data-firehose-aws-cdk/ [アップデート] Amazon Data Firehose に CloudWatch Logs ログイベントからメッセージデータのみを抽出出来るオプションが追加されたので有効にしてみた | DevelopersIO インフラエンジニアが生成AIを活用してログ解析してみた 公式DOC Amazon Data Firehose に解凍された CloudWatch Logs のメッセージ抽出機能を追加 Dynamic partitioning in Amazon Data Firehose - Amazon Data Firehose Supported Lambda blueprints - Amazon Data Firehose Terraform関連 How to enable message extraction in the firehose cloudwatch decompression feature? Terraform Registry
はじめに 株式会社エブリーでソフトウェアエンジニアをしている桝村です。 本記事では、Nuxt 3 へのアップデートに向けて、Nuxt Bridge を使用して Nuxt 2 のアプリケーションへサーバーエンジン Nitro を導入したので、実施内容やそれによって得られた知見について紹介します。 この記事のゴールは、以下を想定しています。 Nitro の概要や、Nuxt 2 への Nitro 導入のメリットを把握する Nuxt 2 への Nitro 導入における変更点や考慮すべきポイントを把握する Nuxt 3 へのアップデートに関連して、Vuex の Pinia への移行については、以下の記事で詳しく紹介しています。 tech.every.tv サーバーエンジン Nitro とは サーバーエンジン Nitro とは、様々な環境で軽量な Web サーバーを構築できるライブラリのことです。 Vue や Nuxt 開発メンバーが中心のプロジェクト unjs が開発・メンテナンスしており、Nuxt 3 へデフォルトで組み込まれています。 UnJS は Unified JavaScript Tools の略で、JavaScript の開発をより効率的かつ柔軟に行うために設計された、一連のオープンソースライブラリおよびツールを提供しているプロジェクトです。 同様のライブラリとして、Nuxt 2 との互換性がある Express.js や Koa.js, Fastify などがあります。 詳しくは、以下のリンクをご参照ください。 nitro.unjs.io Nuxt での Nitro の採用 ここでは、Nuxt での Nitro の採用について、概要やメリットを整理します。 Nuxt での Server の構成や役割 Nitro が採用された Nuxt の Server の構成は以下のようになっています。 Nuxt の Server 構成 nuxt.com Server Engine: アプリケーションのサーバーを動作させるための基盤となる技術 Nuxt: Vue.js や SSR、状態管理 などの機能を提供する高レベルのフレームワーク Nitro: 軽量でポータブルな出力を生成するライブラリ h3: 軽量で高速な HTTP サーバーのライブラリ。Nitro の基盤技術 また、Nuxt の Server 側では以下をはじめとした責務を担っています。 サーバーのビルド・起動設定 API のルーティング初期化 HTTP リクエストの処理 初期 HTML のレンダリング 静的なサーバーサイドコンテンツの生成 (ex. サイトマップ) etc... 上記を踏まえると、Nuxt での Nitro の採用は、大きな変更であることが想定できます。 Nuxt への Nitro 導入のメリット Nuxt への Nitro 導入には以下のようなメリットがあります。 高速なサーバーレスポンス ハイブリッドレンダリングのサポート ホットリロードが高速 詳しくは、以下のリンクをご参照ください。 nuxt.com 実際の Nitro 導入による効果については、後述の結果と振り返りで紹介しております。 Nuxt 2 への Nitro の導入における変更点 前提 今回は、以下の技術スタックを持つ本番運用中のアプリケーションへの導入を想定しております。 Node.js v20.14.0 nuxt v2.17.2 @nuxt/bridge v3.0.1 express v4.17.1 本アプリケーションは、Nuxt Bridge を利用して Nuxt 2 の状態で Nuxt 3 への移行を進めており、今回は移行の一つであるサーバーエンジン Nitro の導入を行いました。 Nuxt Bridge とは、Nuxt 3 と上方互換性があり、Nuxt 3 の機能の一部を Nuxt 2 で利用できるようにするためのライブラリです。 nuxt.com 以降の内容は、公式の移行ガイドを参考にしながらも、実際に対応を進める中でハマったポイントや気づきを中心に紹介していきます。 開発サーバーの起動 Nuxt が Nitro を利用して開発サーバーを起動するには、CLI コマンド nuxi のインストール・利用が必要です。 前提として、 nuxi のインストール・利用には、Node.js のバージョン 18.0.0 以上が必要そうでした。 WARN Current version of Node. js ( 16 . 18 . 0 ) is unsupported and might cause issues. Please upgrade to a compatible version >= 18 . 0 . 0 . その上で、基本的には、以下のリンクを参考に進めていくことができます。 nuxt.com また、開発サーバーの起動設定について、コマンド nuxt では server オプションを利用していましたが、コマンド nuxi では devServer オプションの利用が必要なのもポイントでした。 export default defineNuxtConfig({ - server: { + devServer: { port: 3002, } }) nuxt.com 加えて、コマンド nuxt ではファイルの内容をバッファとして読み込んで渡す仕様でしたが、コマンド nuxi ではファイルのパスを文字列として直接渡す仕様に変更になっていました。より設定が簡潔になったと言えるでしょう。 export default defineNuxtConfig({ devServer: { port: 3002, https: { - key: fs.readFileSync(path.resolve(__dirname, 'server.key')), - cert: fs.readFileSync(path.resolve(__dirname, 'server.crt')) + key: './server.key', + cert: './server.crt' } } }) デプロイメント nuxi では build コマンドを実行することで、 .output ディレクトリにアプリケーションのビルド成果物を出力します。この成果物がサーバーを起動するためのエントリーポイントとなります。 また、デフォルトではポート 3000 でサーバーが起動するのと、上述の devServer オプションを参照しないので カスタマイズでポートを変更したい場合は、以下のように環境変数を利用してポートを指定する必要がありました。 PORT = 3002 node .output/server/index.mjs nuxt.com エンドポイント・ミドルウェアの設定 前提として、Nuxt 2 では express を利用してエンドポイントやミドルウェアを設定していたので、Nitro の導入にあたっては Nitro (h3) の API を利用するように書き換えることが基本的な方針でした。 Express.js から Nitro (h3) へ書き換えする場合 Nitro では h3 の defineEventHandler によりアプリケーションロジックを定義します。 コンテキストにあたる event インスタンスを受け取って、ロジックを実行する関数を定義することができます。 Express.js // server/api/test.ts export default ( req , res , next ) => { // ... Do whatever you want here next(); } Nitro (h3) // server/api/test.ts import { defineEventHandler } from "h3" ; export default defineEventHandler( async ( event ) => { // ... Do whatever you want here } ); nuxt.com 以下は、具体的な書き換え例になります。 Express.js import urlParse from "url-parse" ; export default ( req , res , next ) => { const host = req . headers . host ; const parsedUrl = new URL ( `https:// ${ host }${ req . originalUrl } ` ) ; const pathname = parsedUrl . pathname ; if ( pathname . match (/ . +\ / $ /)) { parsedUrl . pathname = pathname . replace (/ \ / $ / , "" ) ; res . writeHead ( 301 , { Location : urlParse ( parsedUrl ) . href }) ; res . end () ; } else { next () ; } } ; Nitro (h3) import { defineEventHandler , sendRedirect , getRequestURL , getRequestHost } from "h3" ; export default defineEventHandler (( event ) => { const host = getRequestHost ( event ) ; const parsedUrl = new URL ( getRequestURL ( event ) , `https:// ${ host } ` ) ; const pathname = parsedUrl . pathname ; if ( pathname . match (/ . +\ / $ /)) { parsedUrl . pathname = pathname . replace (/ \ / $ / , "" ) ; return sendRedirect ( event , `https:// ${ host }${ parsedUrl . pathname } ` , 301 ) ; } }) ; express では、リクエストオブジェクトから直接情報を取得しているのに対して、h3 では getRequestHost や getRequestURL のような関数を使用してリクエストから情報を取得しています。 それにより、関数の抽象化を通じてコードの可読性と保守性を向上させることに重きを置いていたり、Nuxt 3 で設計を刷新しようとしていることが垣間見えます。 Express.js のコードをそのまま利用する場合 express のコードでも、 fromNodeMiddleware() で変換することで Nitro (h3) でそのまま利用することができました。 Express.js // server/api/index.js import express from "express" ; const app = express () ; app . get ( "/api/test" , ( req , res ) => { res . send ( "Hello World!" ) ; }) ; export default app ; Nitro (h3) // server-middleware/api/index.js import express from "express" ; import { fromNodeMiddleware } from "h3" ; app . get ( "/api/test" , ( req , res ) => { res . send ( "Hello World!" ) ; }) ; export default fromNodeMiddleware ( app ) ; これにより、徐々に express から h3 ベースへコードの書き換えを進めることが可能です。 ただし、Nuxt として推奨されている機能ではないことは留意しておくと良さそうです。 nuxt.com 結果と振り返り 結果 Nuxt 3 への Nitro 導入した結果、体感の部分もありますが、今回のアプリケーションでは以下のような効果が得られました。 # ・ホットリロード: 約 20% 高速化 # ・開発サーバーの起動時間:約 30% 高速化 # ・サーバーのレスポンスタイム: 約 5% 高速化 サーバーのレスポンスタイムについて、今回は開発スピードを優先して主に express のコードをそのまま利用する方針で進めたので、モジュールの変換に伴うオーバーヘッドが影響している可能性があります。 なので、Nitro (h3) へ完全移行できるとさらに改善が見込めるかもしれません。 振り返り サーバーエンジン Nitro の導入に際して、Nuxt のサーバーの基盤技術の刷新でもあり、変更点が多くありました。 一方で、開発サーバーの起動時間やホットリロードの高速化など、開発効率の向上が期待できることもわかりました。 今後は、Nitro (h3) への完全移行を進めることで、更なるパフォーマンス向上や開発効率の向上を図っていきたいと考えています。 おわりに 今回は、Nuxt 3 へのアップデートに向けて、Nuxt Bridge を使用して Nuxt 2 のアプリケーションへサーバーエンジン Nitro を導入したので、実施内容やそれによって得られた知見について紹介しました。 これから Nuxt Bridge を使用して Nuxt 2 のアプリケーションへ Nitro の導入を検討している方にとって、参考になれば幸いです。
はじめに こんにちは。 株式会社エブリーの開発本部データ&AIチーム(DAI)でデータエンジニアをしている吉田です。 今回は、Text-to-SQLを実現するDatabricks Genieを紹介します。 Databricks Genie Databricks Genieは、自然言語を利用してデータ分析が行えるサービスです。 あらかじめデータ、サンプルクエリ、Genieへの指示を登録しておくことで、Genieに対して自然言語でクエリを投げることができます。 これにより、SQLに詳しくない人でもデータ分析を行うことができます。 AI/BI Genie Space とは何ですか? Genieを利用する Genieを利用するためには、以下の手順が必要です。 利用するデータをUnity Catalogに登録する Genie Spaceを作成する Genieをチューニングする 今回は弊社が提供しているレシピ動画サービス、DELISH KITCHENのデータを模したサンプルデータを用意し、Genieを利用してみます。 サンプルデータはランダムに生成したもので、実際のデータとは異なります。 サンプルデータをUnity Catalogに登録する サンプルデータは、以下のような構造です。 テーブル名 カラム 説明 user_master id age gender recipe_master id recipe_name is_premium プレミアムレシピかどうか viewed_video event_date user_id recipe_id seconds 動画視聴時間 referrer_screen 直前に見た画面 Unity Catalogへの登録 Genieではカタログのメタデータを利用してクエリを生成するため、テーブル/カラムのコメントを登録しておく必要があります。 登録にはAI Generate機能を利用することでデータの内容から適切なコメントを生成できるため、利用すると便利です。 コメントの自動生成 Genie Spaceを作成する Genie Spaceは、Genieの利用者がデータ分析を行うためのスペースです。 使用するテーブルやサンプルクエリ、使用するコンピュートリソースなどを指定して作成します。 Genie Spaceの初期設定 Genieをチューニングする Genieに対して、クエリの生成精度を向上させるためのチューニングを行えます。 ドメイン知識を追加したり、回答形式を指定する、あらかじめ質問とSQLをセットで登録し学習させるなど、Genieの精度を向上させる方法があります。 チューニング Genieでデータ分析をする 実際にGenieに対して、日本語で質問を投げてみます。 クエリの実行後、Show Generated Codeをクリックすると、Genieが生成したクエリを確認できます。 最初はシンプルな質問を投げてみます。 回答 よさそうです。 次はテーブルのJoinが発生する質問を投げてみます。 回答 よさそうです。 さらに複数のJoinが発生する質問を投げてみます。 回答 こちらも良さそうです。 では簡単な変換を伴う質問を投げてみます。 回答 うまくいきませんでした。 このように質問が正確に理解されない、または誤ったクエリを生成することがあります。 そういった場合はチューニングを行うことで精度を向上できます。 例えば以下のようなクエリを質問とセットで登録することで、Genieに正しいクエリを生成するよう学習させられます。 クエリと質問の登録によるチューニング この状態で同様の質問をしてみます。 回答 今度は正常にクエリが生成されました。 このようにチューニングを行うことで、Genieの精度が向上します。 まとめ 今回はDatabricks Genieを利用して、自然言語でデータ分析を行う方法を紹介しました。 Databricks Genieを利用することで、SQLに詳しくない人でも質問を入力するだけでデータ分析を行うことができます。 これにより、データ分析の敷居が下がりデータ活用が進むことが期待されます。
はじめに こんにちは。DELISH KITCHEN開発部の村上です。 直近は社内でAmazon Bedrockを使った RAG基盤の構築をしています。その中でちょうど先月AWSから発表された advanced RAG機能 の中のAdvanced parsing optionsを検証も兼ねて使用する機会があったので紹介します。 Advanced parsing optionsとは Knowledge baseではS3や他のデータコネクターを指定し、データソースを作成、同期することによってOpensearch ServerlessといったベクトルDBにデータを格納しています。データソースは さまざまなファイル形式をサポート していますが、今まではサポートしている形式であってもその解析精度に課題が残るものもありました。 今回のアップデートで追加されたAdvanced parsing optionsは有効化することによって、今まで課題であったPDFファイルのテーブルや表、グラフなど非テキスト情報も基盤モデルを使って解析してベクトルDBに埋め込むことができるようになります。 設定可能な値は二つのみでほぼ有効化のみですぐに試すことができます。 使用する基盤モデル 『Claude 3 Sonnet v1』 or 『Claude 3 Haiku v1 』 parserの指示プロンプト 英語のデフォルトプロンプトが設定済み 長いので折り畳みますが、デフォルトプロンプトはこのように記述されています。 プロンプト内容 Transcribe the text content from an image page and output in Markdown syntax (not code blocks). Follow these steps: 1. Examine the provided page carefully. 2. Identify all elements present in the page, including headers, body text, footnotes, tables, visualizations, captions, and page numbers, etc. 3. Use markdown syntax to format your output: - Headings: # for main, ## for sections, ### for subsections, etc. - Lists: * or - for bulleted, 1. 2. 3. for numbered - Do not repeat yourself 4. If the element is a visualization - Provide a detailed description in natural language - Do not transcribe text in the visualization after providing the description 5. If the element is a table - Create a markdown table, ensuring every row has the same number of columns - Maintain cell alignment as closely as possible - Do not split a table into multiple tables - If a merged cell spans multiple rows or columns, place the text in the top-left cell and output ' ' for other - Use | for column separators, |-|-| for header row separators - If a cell has multiple items, list them in separate rows - If the table contains sub-headers, separate the sub-headers from the headers in another row 6. If the element is a paragraph - Transcribe each text element precisely as it appears 7. If the element is a header, footer, footnote, page number - Transcribe each text element precisely as it appears Output Example: A bar chart showing annual sales figures, with the y-axis labeled "Sales ($Million)" and the x-axis labeled "Year". The chart has bars for 2018 ($12M), 2019 ($18M), 2020 ($8M), and 2021 ($22M). Figure 3: This chart shows annual sales in millions. The year 2020 was significantly down due to the COVID-19 pandemic. # Annual Report ## Financial Highlights * Revenue: $40M * Profit: $12M * EPS: $1.25 | | Year Ended December 31, | | | | 2021 | 2022 | |-|-|-| | Cash provided by (used in): | | | | Operating activities | $ 46,327 | $ 46,752 | | Investing activities | (58,154) | (37,601) | | Financing activities | 6,291 | 9,718 | Here is the image. これまでとの挙動の比較 実際にデフォルトの有効化されていない状態と比較しながら、Advanced parsingがどのようにPDFを解析しているのかを確認していきます。 今回はサンプルデータとして 情報通信白書令和5年版 のPDFをS3に入れて解析を行っています。 以下は使用している設定値です。 基盤モデル: Claude 3 Sonnet v1 parserの指示プロンプト: デフォルト グラフの解析 まず、p9の棒グラフを解析してみます。 PDFの中のグラフは、デフォルトの設定だと以下のように解析されました。デフォルトでも大きく崩れてはいませんが、それぞれの文字同士のつながりがわかりにくく、ひとつのグラフとして解釈するのは難しいかもしれません。 違法・有害情報センターへの相談件数の推移 0 1,000 2,000 3,000 4,000 5,000 6,000 7,000 平成22 平成23 平成24 平成25 平成26 平成27 平成28 平成29 平成30 令和元 令和2 令和4令和3 (年度) (件) 1,337 1,560 2,386 2,927 3,400 5,200 5,251 5,598 5,085 5,198 5,407 6,329 5,745 (出典)総務省「令和4年度インターネット上の違法・有害情報対応相談業務等請負業務報告書(概要版)」 Advanced parsingだとこのグラフはマークダウン形式で解析され、それぞれの年と数値の関係がわかりやすく表現されています。(※これ以降ではわかりやすく改行コードで改行を入れていますが、実際には一行にまとまっています。) ## 違法・有害情報センターへの相談件数の推移\n | 年度 | 件数 |\n |-|-|\n | 平成22 | 1,337 |\n | 平成23 | 1,560 |\n | 平成24 | 2,386 |\n | 平成25 | 2,927 |\n | 平成26 | 3,400 |\n | 平成27 | 5,200 |\n | 平成28 | 5,251 |\n | 平成29 | 5,598 |\n | 平成30 | 5,085 |\n | 令和元 | 5,198 |\n | 令和2 | 5,407 |\n | 令和3 | 6,329 |\n | 令和4 | 5,745 |\n (出典) 総務省「令和4年度インターネット上の違法・有害情報対応相談業務等請負業務報告書(概要版)」\n 他にシンプルな円グラフでも同じような検証を行いましたが、同じ結果でAdvanced parsingの方がより構造化されて解釈がしやすくなっていました。では、もう少し複雑なものだとどうでしょうか。 こちらはp7にある似たような折れ線グラフですが、先ほどの棒グラフと違い、細かい各年度の数値が書かれていません。 これをデフォルト設定で読み込むと、先ほどと同じくテキストは読み込みますが、大事な中身のグラフに対する内容が抜け落ちてしまっています。 主要プラットフォーマーの売上高の推移 0 100 200 300 400 500 600 (10億ドル) Google Amazon Meta Apple Microsoft Baidu Alibaba Tencent Holdings 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022(年) (出典)Statistaデータを基に作成 一方のAdvanced parsingだとそれぞれの数値が詳細に出されていないことを判断して、無駄なテキスト情報を省き、グラフが示唆する内容を簡単にまとめて解説しています。惜しいのは内容の抜粋になってしまっているので、RAGとして質問した時にそれ以上の回答はできなそうです。ただ、現状はデフォルトプロンプトを使っているので、カスタマイズすることによって精度向上は期待できるかもしれません。 ## 主要プラットフォーマーの売上高の推移\n この画像は、主要プラットフォーマー企業の過去10年間の売上高の推移を示すグラフです。縦軸は売上高(10億ドル)、横軸は年を表しています。グラフには、Google、Amazon、Meta、Apple、Microsoft、Baidu、Alibaba、Tencent Holdingsの売上高の推移が示されています。全体的に右肩上がりの傾向が見られ、特にGoogleとAmazonの売上高の伸びが顕著です。\n (出典) Statistaデータを基に作成\n 次にp9にあるよりテキスト情報が多く、それぞれの対応関係を正しく把握しないといけないようなものを見てみます。 デフォルト設定では同じように構造化されていない文字の抽出だけで複雑になった分だけよりわかりにくくなっています。 インターネット上の偽・誤情報への接触頻度 毎日、またはほぼ毎日 最低週1回 月に数回 ほとんどない 頻度はわからない 一度も見たことがない そもそも何がフェイクニュースなのかがわからない 19.1 12.0 16.6 18.6 20.2 2.9 10.7 19.5 19.5 21.7 10.7 22.2 5.0 1.4 令和3年度インターネット上のメディア (SNSやブログなど) 令和3年度まとめサイト Advanced parsingだと先ほどのようにマークダウン形式で解析され、一見すると綺麗に整ったように感じられます。 しかし、よく見るとそれぞれに対応する数値が違っていたり、欠損が目立っており正しく解析はできていないようで一部はハルシネーションにつながりそうな結果となりました。 ## インターネット上の偽・誤情報への接触頻度\n | | 令和3年度インターネット上のメディア(SNSやブログなど) | 令和3年度まとめサイト |\n |-|-|-|\n | 毎日、またはほぼ毎日 | 19.1% | 10.7% |\n | 最低週1回 | 12.0% | 19.5% |\n | 月に数回 | 16.6% | 19.5% |\n | ほとんどない | 18.6% | 21.7% |\n | 頻度はわからない | 20.2% | 10.7% |\n | 一度も見たことがない | 2.9% | 22.2% |\n | そもそも何がフェイクニュースなのかがわからない | | 5.0% |\n | | | 1.4% |\n (出典) 総務省「令和3年版 国内外における偽情報に関する意識調査」 画像の解析 p39の埋め込まれた画像を解析してみます。 デフォルト設定では画像は完全な非テキスト情報となり、その部分だけが情報として抜け落ちてしまいました。 図表2-1-4-1 校務・学習データの可視化(Microsoft) (出典)Microsoft 一方でAdvanced parsingでは、画像内で表現されていることを解析して、説明が加えられています。内容も大きく異なることを言っているわけではなく、かなりいい精度で解析ができています。 ## 図表2-1-4-1 校務・学習データの可視化(Microsoft) この図は、Microsoftによる校務・学習データの可視化の概要を示しています。左側には、Microsoft Teamsやアンケート・出欠管理システムなどの学校で利用されるシステムが列挙されています。中央には、これらのシステムから収集されたデータが蓄積されていることが示されています。右側には、蓄積されたデータを可視化し、学校全体、クラス、児童・生徒個人のレベルで分析できることが示されています。 表の解析 p7の表を解析してみます。 デフォルト設定の結果は今までのグラフと同じく、文字の抽出のみです。 プラットフォーマーが取得するデータ項目 データ項目 プラットフォーム Google Facebook Amazon Apple 名前 〇 〇 〇 〇 ユーザー名 - - 〇 - IPアドレス 〇 〇 〇 〇 検索ワード 〇 - 〇 〇 コンテンツの内容 - 〇 - - コンテンツと広告表示の対応関係 〇 〇 - - アクティビティの時間や頻度、期間 〇 〇 - 〇 購買活動 〇 - 〇 - コミュニケーションを行った相手 〇 〇 - - サードパーティーアプリ等でのアクティビティ 〇 - - - 閲覧履歴 購買活動 〇 - 〇 - コミュニケーションを行った相手 〇 〇 - - サードパーティーアプリ等でのアクティビティ 〇 - - - 閲覧履歴 〇 - 〇 - (出典)Security.org「The Data Big Tech Companies Have On You」 より、一部抜粋して作成 Advanced parsingでは、マークダウンでそのまま表形式を表現することでPDFと同じ構造を保てています。 ## プラットフォーマーが取得するデータ項目\n | データ項目 | Google | Facebook | Amazon | Apple |\n |-|-|-|-|-|\n | 名前 | ◯ | ◯ | ◯ | ◯ |\n | ユーザー名 | - | - | ◯ | - |\n | IPアドレス | ◯ | ◯ | ◯ | ◯ |\n | 検索ワード | ◯ | - | ◯ | ◯ |\n | コンテンツの内容 | - | ◯ | - | - |\n | コンテンツと広告表示の対応関係 | ◯ | ◯ | - | - |\n | アクティビティの時間や頻度、期間 | ◯ | ◯ | - | ◯ |\n | 購買活動 | ◯ | - | ◯ | - |\n | コミュニケーションを行った相手 | ◯ | ◯ | - | - |\n | サードパーティーアプリ等でのアクティビティ | ◯ | - | - | - |\n | 閲覧履歴 | ◯ | - | ◯ | - |\n (出典) Security.org「The Data Big Tech Companies Have On You」より、一部抜粋して作成\n テキストの解析 基本的にAdvanced parsingはこれまで述べてきた非テキスト情報の解析が大きな特徴になっていますが、テキスト情報でもどのような影響があるのかを最後にこちらの目次を解析した結果で確認します。 デフォルト設定では、チャンキングの設定により途中で切れてしまっていますが、表示されている通りに抽出を行っているため無駄にトークンを消費していそうです。 第1章 データ流通の進展 第1節 データ流通を支える通信インフラの高度化・・・・2 ■1  固定通信・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・2 ■2  移動通信・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・2 第2節 データ流通とデジタルサービスの進展・・・・・・・・5 ■1  片方向のデータ発信・ (Web1.0時代:1990年代~2000年代前半)・・・・・5 ■2 一方のAdvanced parsingでは書かれている内容を解析して、その意味を失わない範囲でマークダウンに整形しており、無駄がありません。 また、マークダウンで構造も表現できているため、文章の関係もこちらの方がわかりやすいです。 ### 第1章 データ流通の進展 #### 第1節 データ流通を支える通信インフラの高度化 * 1  固定通信 * 2  移動通信 #### 第2節 データ流通とデジタルサービスの進展 * 1  片方向のデータ発信 * (Web1.0時代:1990年代~2000年代前半) * 2  双方向のデータ共有 * (Web2.0時代:2000年代後半~) 運用上の注意点 以上のようにAdvanced parsing optionを有効化することによって完璧ではないものの全体の精度の向上が見込めそうなことがわかりました。一方で使っていく中で以下のような注意点があります。 基盤モデルで前処理を行う都合上、デフォルト設定よりモデルの利用コストが増加する ベクトルDBへの同期処理が遅いため、リアルタイムに文書を処理したい場合に適していない デフォルトプロンプトが英語だからなのか、そのままデフォルトで使うと一部の解析結果が英語になってしまう可能性がある データサイズ、読み込みファイル数に 制限 がある 特にフルにこの機能をRAG基盤で活用するにあたって大きな障壁に感じたのは、読み込みファイル数の制限です。現状では解析できるファイルの最大数は100ファイルで申請による調整もできません。工夫したとしてもナレッジベースあたりのデータソース数は5つなので、分割して全て使ったとしても500ファイル程度で上限に達してしまいます。もう一つの解決策としてファイルサイズの上限まで複数のファイルを繋げて、ファイル数を削減することですが、その手間を考えるとAWS側での今後の引き上げを期待したいところです。 まとめ 今回はここ1ヶ月ほどで出たAdvanced parsing optionsを活用したKnowledge baseの精度向上に関して、その挙動を見ていきながら機能を紹介しました。 リリースではこの他にもチャンキング戦略のアップデートやクエリ分割の機能など多くのアップデートがあり、Amazon Bedrockの改善のスピード感とAWSがかなり力を入れていることを日々感じています。現在同じようにRAG基盤を構築されている方はぜひ追加された機能も試してみてください。
概要 TIMELINE開発部の内原です。 本日は、改めてGo言語におけるgoroutine間での通信方法について整理してみました。 Go言語ではgoroutineを用いて簡単に並行処理を記述することができます。またその際、goroutine間で通信を行い、情報のやり取りをしたり互いに協調しつつ動作することもできます。 ただ、通信する手段自体は複数あり、それぞれ特徴がありますので、どのようなことを実現したいのかによって適切な方法を採用する必要があります。 いきなり結論 先に結論を書いておくと、以下のような切り分け方になりそうです。 やりたいこと 実装 goroutine間で値を共有したい sync.Mutex を使う goroutineから任意のタイミングで通知したい(通知のみでよい場合、かつ一度きり) sync.WaitGroup を使う goroutineから任意のタイミングで通知したい(通知のみでよい場合、かつ複数回) sync.Cond を使う goroutineから任意のタイミングで通知したい(なんらか値を返却したい場合) channel を使う 特定のタイミングでgoroutineを終了させたい context.Context を使う それぞれの実装を記載します。 それぞれの実装方法 goroutine間で値を共有したい sync.Mutex は排他制御を実現します。 Lock されている間は他の Lock をブロックします。ブロックを解除するには Unlock を呼び出します。なお sync.WaitGroup については後述します。 func increment(wg *sync.WaitGroup, mtx *sync.Mutex, cnt * int ) { mtx.Lock() defer mtx.Unlock() *cnt++ wg.Done() } func sampleMutex() { wg := sync.WaitGroup{} mtx := sync.Mutex{} cnt := 0 for i := 0 ; i < 10000 ; i++ { wg.Add( 1 ) go increment(&wg, &mtx, &cnt) } fmt.Printf( "waiting for goroutine complete... \n " ) wg.Wait() fmt.Printf( "completed, cnt: %d \n " , cnt) } 実行結果は以下です。 並行実行されていても正しく排他制御が行われ、想定した値に更新されていることが分かります。 waiting for goroutine complete... completed, cnt: 10000 もしmutexを使わないで実行した場合、以下のように想定した値に更新されないことがあります。 この場合いわゆる競合状態になっていることが分かります。このような状態だと場合によってはプログラムがクラッシュすることもあるため、gouroutine間で共有するリソースにアクセスする場合は排他制御が必要です。 waiting for goroutine complete... completed, cnt: 9382 なお、 sync.RWMutex というものもあり、こちらは書き込み用ロック Lock と読み込み用ロック RLock とが分かれており、読み込みロック同士ならば並行で実行できるという違いがあります。 goroutineから任意のタイミングで通知したい(通知のみでよい場合、かつ一度きり) sync.WaitGroup は Add された数と同数の Done が行われるまで Wait で待機します。これにより、呼び出されたgoroutine側の適当なタイミングで通知を行うことができます。 つまりこの機能における通知とは一方向かつ一度きりと言えます。このため、一般的には呼び出したgoroutineが終了したことを呼び出し元に伝える用途で使われることが多いと思います。 func procGoroutine(wg *sync.WaitGroup, n int ) { fmt.Printf( "goroutine: %d \n " , n) time.Sleep( 1 * time.Second) wg.Done() } func sampleWaitGroup() { var wg sync.WaitGroup for i := 0 ; i < 3 ; i++ { wg.Add( 1 ) go procGoroutine(&wg, i) } fmt.Printf( "waiting for goroutine to complete... \n " ) wg.Wait() fmt.Printf( "completed \n " ) } 実行結果は以下です。 呼び出したgoroutineが終了するまで呼び出し元が待機していることが分かります。 waiting for goroutine to complete... goroutine: 0 goroutine: 1 goroutine: 2 completed goroutineから任意のタイミングで通知したい(通知のみでよい場合、かつ複数回) sync.Cond はいわゆる条件変数で、goroutine間でなんらかのタイミングで通知のみを複数回行いたい場合に利用できます。 Wait した側は Signal または Broadcast されるまで待機することができますが、その際値の受け渡しはできません。 値の受け渡しが必要な場合、別途共有リソースを用意して値をやり取りするか、後述する chan を用いることになります。 以下はいわゆるProducer/Consumerの機構を実装したものです。 producerはデータを生産しますが、一定量になったらconsumerが消費するのを待ちます。 consumerはデータを消費しますが、存在しない場合はproducerが生産するのを待ちます。 func produce(cond *sync.Cond, messages *[] string , msg string ) { cond.L.Lock() for len (*messages) == 5 { fmt.Printf( "produce: messages full, msg: %s \n " , msg) cond.Wait() } *messages = append (*messages, msg) cond.Signal() cond.L.Unlock() } func consume(cond *sync.Cond, messages *[] string ) { cond.L.Lock() for len (*messages) == 0 { fmt.Printf( "consume: messages empty \n " ) cond.Wait() } msg := (*messages)[ 0 ] fmt.Printf( "consume: msg: %s \n " , msg) *messages = (*messages)[ 1 :] cond.Signal() cond.L.Unlock() } func sampleCond() { wg := sync.WaitGroup{} mutex := sync.Mutex{} cond := sync.NewCond(&mutex) messages := make ([] string , 0 ) wg.Add( 2 ) go func () { for i := 0 ; i < 10 ; i++ { produce(cond, &messages, fmt.Sprintf( "msg %d" , i)) } wg.Done() }() go func () { for i := 0 ; i < 10 ; i++ { consume(cond, &messages) } wg.Done() }() wg.Wait() } 実行結果は以下です。 producerとconsumerとがそれぞれ協調しつつ動作していることが分かります。 consume: messages empty produce: messages full, msg: msg 5 consume: msg: msg 0 consume: msg: msg 1 consume: msg: msg 2 consume: msg: msg 3 consume: msg: msg 4 consume: messages empty consume: msg: msg 5 consume: msg: msg 6 consume: msg: msg 7 consume: msg: msg 8 consume: msg: msg 9 goroutineから任意のタイミングで通知したい(なんらか値を返却したい場合) channelは、送信元と送信先とで任意のデータをやり取りすることができる機構です。その際、channelに読み込めるデータが存在するかどうかをあらかじめチェックすることも可能なので、さまざまな用途に利用することができます。 先ほどのProduder/Consumerをchannelで実装し直したものが以下です。 func produce(messages chan <- string , msg string ) { for { select { case messages <- msg: return default : fmt.Printf( "produce: messages full, msg: %s \n " , msg) time.Sleep( 100 * time.Millisecond) } } } func consume(messages <- chan string ) { for { select { case msg, ok := <-messages: if !ok { return } fmt.Printf( "consume: msg: %s \n " , msg) default : fmt.Printf( "consume: messages empty \n " ) time.Sleep( 100 * time.Millisecond) } } } func sampleChannel() { messages := make ( chan string , 5 ) go func () { for i := 0 ; i < 10 ; i++ { produce(messages, fmt.Sprintf( "msg %d" , i)) } close (messages) }() consume(messages) } 実行結果は以下です。 sync.Cond の時と同様の結果になっていることが分かります。 consume: messages empty produce: messages full, msg: msg 5 consume: msg: msg 0 consume: msg: msg 1 consume: msg: msg 2 consume: msg: msg 3 consume: msg: msg 4 consume: messages empty produce: messages full, msg: msg 5 consume: msg: msg 5 consume: msg: msg 6 consume: msg: msg 7 consume: msg: msg 8 consume: msg: msg 9 特定のタイミングでgoroutineを終了させたい context.Context を使うことで、呼び出し元での状態変化を呼び出し先に通知することが可能です。 一般的にはエラーハンドンリング時に用いられることが多いと思われます。goroutineの呼び出し元でなんらか異常やタイムアウトが発生した場合などに、呼び出し先でも同様に終了してほしいといったケースで利用できます。 例によってProducer/Consumerでcontextを組み込みます。 func produce(messages chan <- string , msg string ) { for { select { case messages <- msg: return default : fmt.Printf( "produce: messages full, msg: %s \n " , msg) time.Sleep( 100 * time.Millisecond) } } } func consume(ctx context.Context, messages <- chan string ) { for { select { case <-ctx.Done(): fmt.Printf( "consume: context cancelled \n " ) return case msg, ok := <-messages: if !ok { return } fmt.Printf( "consume: msg: %s \n " , msg) time.Sleep( 100 * time.Millisecond) default : fmt.Printf( "consume: messages empty \n " ) time.Sleep( 100 * time.Millisecond) } } } func sampleContext() { messages := make ( chan string , 5 ) ctx, cancel := context.WithTimeout(context.Background(), 300 *time.Millisecond) defer cancel() go func () { for i := 0 ; i < 10 ; i++ { produce(messages, fmt.Sprintf( "msg %d" , i)) } close (messages) }() consume(ctx, messages) } 実行結果は以下です。 consumeは時間のかかる処理になっていますが、途中でcontextの終了を検知して処理を中断していることが分かります。 consume: messages empty produce: messages full, msg: msg 5 produce: messages full, msg: msg 5 consume: msg: msg 0 consume: msg: msg 1 produce: messages full, msg: msg 7 produce: messages full, msg: msg 7 consume: msg: msg 2 consume: context cancelled まとめ 本日はGo言語の並行処理において、goroutine間で情報をやり取りする方法についてまとめました。 これらの機能は便利かつ簡単に使えてしまうのであまり深く考えずに使っていたりしたのですが、記事を書くにあたって改めて学び直すことでより理解を深めることができました。
参考 https://developer.apple.com/jp/videos/play/wwdc2024/10170/ https://github.com/swiftlang/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md ~Copyableの導入 Swift 5.9でCopyableと~Copyableが導入されました。 全ての型が暗黙的にCopyableに準拠するので、今まで通りCopyableを前提にするなら特にCopyableと~Copyableを意識しなくても問題はありませんが、一方、~Copyableを使うと一意なリソースを表現でき、それによってより効率的なコードを書けたり、論理的に正しいコードを書くのに役立つ場合があります。 この記事では、~Copyable導入の利点についての調査と、弊社プロダクトへの導入の検討状況をご紹介します。 CopyableなStructとの比較 Structは値型です。値のコピーが複数作られるので、一意なリソースを表現するのには適していません。 また、値のコピーはメモリを占有しコピー作業に処理時間がかかるので、~CopyableなStructを使うことでコンピューティング資源の消費が少ない効率的なコードを書ける可能性があります。 Classとの比較 Classは参照型です。一意なリソースを表現することができます。 複数の箇所から同時に参照、変更が可能です。そのため、非同期的な処理でデータ競合の問題を起こしたり、参照元の管理のミスでメモリリークを発生させる恐れがあるなどコードを複雑化させる要因になります。 ~CopyableなStructを使うことでよりシンプルで安全なコードを書ける可能性があります。 所有権 ~Copyableな型の値の所有者を明確にするために、 所有権(ownership) という概念が導入されています。 値の受け渡しをするとき、値をコピーする代わりに所有権の移動もしくは共有が行われます。 所有権の取り扱い方には以下の3つの種類があります。 consume 値の所有権を移動する 所有権が移動した後、元の値は無効になる borrow 値の所有権を共有する 元々の所有者から値の所有権が剥奪されることはない 借りた側は値を参照だけできる。値を変更することはできない mutating (or inout) 値の所有権を一時的に移動する 操作が終わるまでの間、値の参照、変更ができる 操作終わったら元の所有者に所有権が戻る ~Copyableを使ったコードの記述例 ~Copyableな型の宣言 struct FloppyDisk : ~ Copyable {} 所有権の移動 値の代入操作をすると、値がコピーされる代わりに所有権が移動します。 system が持っていた所有権は消費され、使用できなくなります。 func copyFloppy () { let system = FloppyDisk() // error: 'system' used after consume let backup = system // consumed here load(system) // used here // ... } func load (_ disk : borrowing FloppyDisk) {} 関数の引数として~Copyableな型の値を渡す場合 関数の引数として~Copyableな型の値を渡す場合、 consuming , borrowing , inout のいずれかを指定する必要があります。 consuming 関数を呼んだ時点で値の所有権が移動します。 値は関数の中で消費される必要があります。 この例では、関数の呼び出し元ではすでに所有権を失っているのに値を消費する操作をしようとしているため、コンパイルエラーになります。 struct FloppyDisk : ~ Copyable { } func newDisk () -> FloppyDisk { let result = FloppyDisk() // error: 'result' consumed more than once format(result) // consumed here return result // consumed again here } func format (_ disk : consuming FloppyDisk) { // ... } borrowing 関数内では引数として受け取った値を変更できません。 この例では関数内で値を消費する操作をしようとしているため、コンパイルエラーになります。 struct FloppyDisk : ~ Copyable { } func newDisk () -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format (_ disk : borrowing FloppyDisk) { // error: 'disk' is borrowed and cannot be consumed var tempDisk = disk // consumed here // ... } inout 関数内では一時的に所有権を持つため値を変更可能です。 関数の終了後は呼び出し元に所有権が戻ります。 この例では、format関数内で値を変更し、関数の終了後に変更された値を利用できています。 struct FloppyDisk : ~ Copyable { } func newDisk () -> FloppyDisk { var result = FloppyDisk() format( & result) return result } func format (_ disk : inout FloppyDisk ) { var tempDisk = disk // ... disk = tempDisk } ~Copyableな型のインスタンスメソッド ~Copyableな型のインスタンスメソッドには、 borrowing , consuming , mutating のいずれかを付与します。 borrowing がデフォルト値のため、明示しない場合は borrowing として扱われます。 ~Copyableの利用例 WWDCのセッション ( https://developer.apple.com/jp/videos/play/wwdc2024/10170/ ) では、~Copyableな型を使って一意なリソースを表現することで、静的に論理的に正しいコードを書く例を紹介しています。 BankTransfer(銀行振込)を題材にしています。 ~Copyableを使わない例 class BankTransfer { func run () { // .. do it .. } } func schedule (_ transfer : BankTransfer , _ delay : Duration ) async throws { if delay < .seconds( 1 ) { transfer.run() // A } try await Task.sleep( for : delay ) transfer.run() } ~Copyableを使わない記述例です。 このコードにはバグがあります。Aの箇所に本来returnが必要なところ、書き忘れています。これによってdelayが1秒未満の場合 transfer.run() が2回実行されてしまいます。 振込の実行(run)は一つのBankTransferに対して複数回行われてはいけないのですが、この例ではそのような制約を実装していないため、呼び出し側の誤った実装によってこのような問題が発生してしまいます。 BankTransferの状態管理を追加した例 class BankTransfer { var complete = false func run () { assert( ! complete) // .. do it .. complete = true } deinit { if ! complete { cancel() } } func cancel () { /* ... */ } } func schedule (_ transfer : BankTransfer , _ delay : Duration ) async throws { if delay < .seconds( 1 ) { transfer.run() } try await Task.sleep( for : delay ) transfer.run() } この例ではBankTransferの状態管理を追加し、run()の複数回実行を防いでいます。 ただし、assertによる動的な検証のため実行時にしか問題を検出できません。 適切なテストを書かない限り見つけられず(この場合、delayが1未満の条件のテストが必要)もしテストで見つけられないと、本番でクラッシュしてしまいます。 ~Copyableを使った例 struct BankTransfer : ~ Copyable { consuming func run () { // .. do it .. discard self } deinit { // .. do the cancellation .. } } ~CopyableなBankTransferを定義しています。 インスタンスメソッドに付与した consuming キーワードによって、run()が複数回実行されることを防いでいます。 func schedule (_ transfer : consuming BankTransfer, _ delay : Duration ) async throws { // error: 'transfer' consumed more than once if delay < .seconds( 1 ) { transfer.run() // consumed here } try await Task.sleep( for : delay ) transfer.run() // consumed again here } バグのあるschedule関数です。 transfer.run() が複数回実行される可能性があることをコンパイル時に検出できています。 func schedule (_ transfer : consuming BankTransfer, _ delay : Duration ) async throws { if delay < .seconds( 1 ) { transfer.run() return } try await Task.sleep( for : delay ) transfer.run() } returnを追加し、バグを修正できました。 このように、~Copyableな型を使って一意なリソースを表現することで、論理的な問題点をコンパイル時に検出できるようになりました。 問題を早期に発見でき、コードの品質向上に役立ちそうです。 弊社プロダクトで~Copyableを適用する 弊社プロダクトではまだ~Copyableを導入していませんが、以下のような箇所で導入することを検討中です。 ネットワーク接続 ネットワーク接続を一意なリソースとして表現する PDF出力 PDF出力ではサイズの大きいデータを扱うため、~Copyableを使うことで不用意なメモリの消費を防止する 広告表示 広告を一意なリソースとして表現する 広告リクエスト、インプレッション、クリックなどの一連の処理を~Copyableな型のインスタンスとして表現する
エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。 エブリーでは retail HUB という複数のプロダクトからなる小売業界向けのサービスの開発を行っています。以前まではサービス開発チームは単一で、その中で複数のプロダクトを開発を行ってきましたが、今後複数のチームがプロダクトごとに分かれて開発を行うこととなり、その体制の変化に伴って Terraform のルートモジュールを分割した話をします。 分割前のモジュール構成と、その問題点 今回紹介する作業を行う以前は、環境 (dev, prd) ごとに存在する単一のルートモジュールから各プロダクト向けのリソースを含むモジュールを参照する形でした。ルートモジュールが単一の Backend 定義を参照し、State ファイルも単一です。 このような構成だとシンプルな反面ルートモジュールに含むリソースが肥大化しやすく、plan / apply 等にかかる時間が長くなりやすいのですが、DELISH KITCHEN などの他の弊社のプロダクトと比較して、向こう数年のリソースの増え方は限定的になりそうなことと、単一のチームでの作業に最も適した形をとりたくこの形を取ってきました。 ところが開発チームや予算管理を分けようと思うと次の問題点が生じ、構造を見直すことにしました。 他プロダクトの変更との競合への対処 Terraform の State と実際のリソースに差分が発生した場合、他チームへの差分の解消を依頼するなどのコミュニケーションコストがかかる 一括で AWS タグを付与することができず、リソースの管理者が不明確になる AWS タグの一括付与のためには AWS provider ブロックの default_tags を使う 必要があり、単一のルートモジュールだとこの方法が採れない AWS の料金をプロダクトごとに分けて確認できないことにもなる 具体的には次のようなディレクトリ構造をしていました: . ├── README.md └── aws ├── envs │ └── dev # dev 環境のルートモジュールや、また環境全体に関わる設定を配置 │ │ ├── backend.tf # AWS S3 を使用しています │ │ ├── main.tf # ルートモジュール。このファイルから aws/modules 配下に定義してある各 Child Module を読み込む │ │ ├── provider.tf │ │ └── variables.tf │ └── prd │ ├── -- 省略 -- │ └── modules ├── common # 特定のプロダクトに依存しないリソースを配置 │ └── dev # 特定のプロダクトに依存しない、dev 環境用のリソースを配置 │ └── cloudtrail │ ├── main.tf # output values 以外のリソースを定義 │ └── outputs.tf # 他 module で使用する output values を定義 └── product_A # product_A 用のリソースを配置 │ └── dev # product_A 用の、dev 環境用のリソースを配置 │ │ ├── ecs │ │ │ └── main.tf │ │ │ └── outputs.tf │ │ │ │ │ -- 省略 -- │ │ │ │ │ └── vpc │ │ ├── main.tf │ │ └── outputs.tf │ └── prd # product_A 用の、prd 環境用のリソースを配置 │ ├── -- 省略 -- │ └── product_B # product_B 用のリソースを配置 │ ├── -- 省略 -- Style Guide - Configuration Language _ Terraform _ HashiCorp Developer に記載例に近しく、あまり特殊な点はありません。Backend に AWS S3 を使用しており、Workspaces を使用せずにモジュールごとにディレクトリを分けて State ファイルも環境ごとに 1 つ存在する構造です。 Terraform ルートモジュールを分割することに 問題点の解消にあたり、プロダクトごとに AWS アカウントを分離し HCL を管理する Git リポジトリごと分割する事も考えましたが、プロダクト同士が将来的に連携/統合する可能性を考えたときに都合が悪くなることを避けたく、AWS アカウントは同一のまま Terraform ルートモジュールをプロダクトごとに分割することとしました。 この変更によって向き合いのプロダクト以外との競合を気にせずに作業を行うことができ、また AWS provider ブロックをプロダクトごとに定義できます。 よって各リソースにプロダクト別のタグを付与できるので プロダクトごとに料金を分けて確認できる ことにもなり、今回の問題点が解消できます。 変更後のディレクトリ構成は下記のようになりました: . ├── README.md └── aws ├── common # 特定のプロダクトに依存しないリソースを配置。cloudtrail など │ └── dev │ ├── cloudtrail │ | ├── main.tf │ | └── outputs.tf │ ├── backend.tf │ ├── main.tf │ ├── provider.tf │ └── variables.tf ├── product_A # product_A のリソースを配置 │ └── dev # 環境ごとにディレクトリを切る │ | ├── s3 # 各 Child Module は環境ごとのディレクトリ配下へ配置 │ | | ├── main.tf │ | | └── outputs.tf │ | | # 他 Child Module ごとのディレクトリがここに並ぶ │ | ├── backend.tf │ | ├── main.tf # product_A 用のルートモジュール │ | ├── provider.tf │ | └── variables.tf │ └── prd │ ├── -- 省略 -- │ └── product_B │ ├── -- 省略 -- 以下、その際に行った作業を簡単に説明します。 1. 新ルートモジュールへと移動予定の Child Module を State から取り除く すでに存在するルートモジュールの State から、新ルートモジュールへと移動予定の module を一旦 terraform state rm します。 $ terraform state rm \ module.cloudtrail \ module.s3 \ # 省略 2. ルートモジュールのファイルを分割し、ディレクトリ構造を変更 単一のルートモジュールだった main.tf を分割し、プロダクトごとにディレクトリを分けて main.tf もそれぞれ置きます。合わせて各 Child Module をそのルートモジュール配下のディレクトリへと移動します。 ディレクトリの移動作業は基本的にはディレクトリを単に mv し、ルートモジュールからの参照先のファイルパスを変更するような単純な内容ですが、他のルートモジュールへ移動してしまったモジュールの Output value への参照はできなくなり、その際は variable や locals を定義して対処療法的にハードコードしました。ここは(試せていませんが) terraform_remote_state Data Source を使用して他の State を参照するように出来ると思います。 3. 新ルートモジュールへと移動するリソースを import する まだ新ルートモジュールの State は空のままなので、 terraform import コマンドを使って先ほど terraform state rm したリソースを新ルートモジュールへと追加していきます。 $ terraform import module.api-gateway.aws_api_gateway_account.this api-gateway-account $ terraform import module.cloudtrail.aws_cloudtrail.default arn:aws:cloudtrail:<region>:<account_id>:trail/default # 省略 import したら、最後に terraform plan で差分がないことを確認して作業終了です。 リソースを State から削除する際は terraform state rm コマンドを Child Module ごとに実行できたためコマンドの数も少なくコマンドの内容もシンプルでした。一方 terraform import ではリソースごとにコマンドを実行する必要があり、また引数の指定方法もリソースの種別ごとに違いがあるためにこのコマンドの作成が割と大変です。 私達の場合は import 対象のリソースがまだ 30 個ほどであったため目立った問題にはなりませんでしたが、リソース量が多いとコマンドを作成・実行する量が膨大になると思います。 後日談: 次に同じことをやるなら、、 今回の作業の終わりかけの段階で、公式記事による State ファイルの分割方法を見つけました。 support.hashicorp.com 記事ではモジュール構成は変えず、単に terraform state mv コマンドの -state , -state-out オプションで Child Module ごとに State ファイルを移動させる方法が書かれています。 私達の場合はルートモジュール分割にあたり、 先に terraform state rm で新ルートモジュールにて管理したいリソースを既存のルートモジュールから取り除く 空のルートモジュールを作成する そこへ terraform import し、新ルートモジュールにて管理したいリソースを新ルートモジュールへ import しなおす ような作業を行いましたが、 先に State ファイルを記事記載の方法で分割する 次に新ルートモジュールを、分割済みの State ファイルを使う形で作成する あわせて、既存ルートモジュールから新ルートモジュールで管理するリソースの記述を削除する するアプローチを取れると、 terraform import コマンドをリソースごとに実行する必要がなく作業量を減らせたかもしれません。もしまた同じようなことを行うのであれば、その方法を試してみたいと思います。 さいごに ここまでお読みいただきありがとうございました。開発チームも複数になり、 retail HUB の開発もこれまで以上に活発になっています。そんなチームに興味がある方、ぜひ一度お話しましょう! corp.every.tv
はじめに こんにちは、株式会社 エブリー の24新卒の蜜澤、きょー、新谷です。 今回は、2024年5月から7月にかけて開催された日本CTO協会主催の合同新卒研修に参加した際の内容と学びについてご紹介します。 合同新卒研修とは 本研修は、日本CTO協会が主催する新卒エンジニア向けの合同研修です。新卒エンジニアが業界全体・企業横断で育てられる試みとして、今年から開催されました。 日本CTO協会が新卒エンジニアを業界全体・企業横断で育てる試み「新卒エンジニア向けの合同研修」を5月29日から実施 上記記事にも書いてありますが、開催に至る背景として 現状企業が経営・事業を大きくする、新たなチャレンジをする上でもエンジニアが常に足りない状況にある スタートアップや中小企業が新卒エンジニアを採用するには、採用コストだけではなく育成のコストも大きくかかる といった課題があることから、本研修が開催されています。 また講義は毎回オフラインで行われ、その後開催される懇親会では、他社の新卒エンジニア同士で交流を深めることができました。(懇親会では毎回お酒と軽食が用意されているので、和気あいあいとした雰囲気で交流ができました!) 研修内容 研修回 講義内容 講師 第1回 キャリア戦略・フォロワーシップとマネジメント 日本CTO協会 / 株式会社LayerX 第2回 Git基礎ハンズオン 株式会社Progate 第3回 BtoB SaaS開発基礎 株式会社アンチパターン 第4回 インテリアコーディネートで学ぶアジャイル開発 株式会社メンバーズ 第5回 BigQueryで始めるデータ分析入門 & 生成AIを活用した分析効率化 グーグル・クラウド・ジャパン合同会社 第6回 AWS研修 アマゾンウェブサービスジャパン合同会社 第7回 サーバ解体研修 GMOペパボ株式会社 第8回 生成AIに関する講義 日本マイクロソフト株式会社 第9回 日本CTO協会ISUCON新卒研修 + 解説 株式会社PR TIMES 24卒の新卒エンジニア3人でこの研修に参加させていただいので、1人1つずつ講義の内容をピックアップし、それぞれの講義内容と学びについて紹介します。 キャリア戦略・フォロワーシップとマネジメント 開発本部のデータ&AIチームの蜜澤です。 私からは「第1回:キャリア戦略・フォロワーシップとマネジメント」の紹介です。 講義を担当してくださったのは、日本CTO協会 理事 / 株式会社LayerXの松本CTO( @y_matsuwitter )です。 この講義は 前編:投資的に考えるキャリアのあり方 後編:フォロワーシップとマネジメント の2つのパートに分かれていたので、それぞれのパートについての学びを紹介します。 前編:投資的に考えるキャリアのあり方 信頼や信用、知識や経験、時間といった自分が持っている「人生の資産」を、自分の仮説や学びたい方向性に向けて効率よく投資し、資産を拡大させていくという「投資家的に考えるキャリアのあり方」について学びました。 今自身に必要な資産を考え、自分の手元の投資可能な資産(お金、時間、信用など)をどのように配分し、どのようなリターンを期待するのか、抱えるリスクはなにかというポートフォリオを作成し、資産を拡大していくことが重要とのことでした。 何を投資するのか? 新卒の私には、たいした資産(特に知識や信用)はありません。 いったい何を投資すれば、、、 「時間」です! 家庭がなく(現在独身・一人暮らし)、健康上の問題も特にない20代の今が人生で最も可処分時間が多いです。 この20代の時間を最大限未来に投資していくことの重要性を松本さんは強調されていました。 「ドキッ!」 とした人も多いのではないでしょうか。 私もその一人です。 可処分時間を未来に投資していくことの重要性を理解しても、私のような遅延評価(必要になったらやる)型の人間は、今必要でないことはサボってしまうことがあると思います、、、 そんな時に努力を習慣化し、投資を続ける助けとなるのが「コミュニティ」のようです! コミュニティの力 「コミュニティの平均値が自分」や「周りの5人の平均が自分」などの言葉はよく耳にします。 松本さんが所属していたコミュニティから次々とCTOが誕生したという話を伺い、より自分が所属するコミュニティの重要性を感じました。 そして、コミュニティにおいて特に重要だと思ったのが「健全な嫉妬」です。 仲間の昇進や技術的な成長を目の当たりにすると、私たちは「あいつには負けられない」という健全な嫉妬を感じます。 「健全な嫉妬心を努力の習慣化に繋げることで成長する」というのがコミュニティの力だと思いました。 幸いにも私の周りには同期や、CTO研修で出会った同世代のエンジニアといった切磋琢磨できる仲間がいます! この中で一番になるという気持ちで、努力(投資)を続けていかなければと考えさせられる前編でした。 後編:フォロワーシップとマネジメント マネージャーの仕事は、単に管理するだけではなく、チームの成果を最大限高めることだと学びました。 チームで大きな成果を出すために チームの成果は、一人一人のモチベーションと能力の掛け算の総和であり、全員がモチベーション高く、かつ専門性を発揮できると大きな成果が出ると松本さんは考えているようです。 そのモチベーションのために、一人一人が納得感と自信を持てるように、意思決定をし、内容を言語化して伝え、メンバーを導くのがマネージャーの重要な役割のようです。 役割は分かりましたが、実際にやるのとても難しそうだなと感じました。 メンバーのモチベーションの源泉を考え、納得感を持ってもらうためにメンバーのことをよく理解するとういう難しい役割を日々担っていただいているマネージャーには頭が上がりません。 マネージャーを支えるために私たちにできることも教えていただきました! こまめな自己開示 マネージャーとうまく連携するには、自己開示が重要だそうです。 自己開示のコツは、何が楽しいか、何を目指したいか、何がわからないか、何が目標でどのような状況かをこまめに伝えることだとわかりました。 わからないことは頻繁に聞きますが、「何が楽しいか」や「目標と今の状況」などはこまめに伝えられていないなと反省しました。 「私自身が自己開示したつもりになっていても、マネージャーには伝わっていなかった」ということはよくありそうだなと思い、自分のことをどれだけ理解してもらえているのか確認しつつ、こまめにコミュニケーションをとっていくことが大切だなと思いました。 マネージャーを理解する 自己開示だけではなく、マネージャーを理解することも大切だと教えていただきました。 「あなたのマネージャーの目標はなんですか?」と聞かれましたが、この時の私はわかりませんでした。 マネージャーを理解することで自分が何をすべきかわかると思ったので、1on1やチームでのランチ会の時に「マネージャーの目標」や「休みの日に何をしているのか」などを聞いてみました! ということで、少しだけ私のマネージャーについて紹介します!! 私のマネージャー 短期目標:マネジメントしながら手を動かすプレイングマネージャー的に技術を高めていきたい 長期目標:全体的に組織を見て、率いていきたい チーム目標:各自の専門性を深めつつ、専門性+αのことをできる組織にする 休日の過ごし方:お子さんと遊ぶことが多いが、最近は暑いので大変 まだまだ理解不足ですが、実際に聞いてみたことで、私はこれから専門性(データ系)に加えて、+α(バックエンドやインフラ等)の知識も身につけていく必要があるということを認識できました。 私のチームにはマネージャー以外にも上司が4人おり、マネージャー以外の方々のことも理解する必要があると思うので、コミュ二ケーションを頻繁に取っていこうと思います! フォロワーシップはマネジメントのスタートライン マネージャーを支えるとは、マネージャーを理解しようとすることで、その視点を手に入れるということは、自身がマネジメントへ進む準備運動であると学びました。 将来的にはマネジメントにも関わりたいと思っているので、フォロワーシップを磨き、準備運動を完了させようと思います! 今後に向けて この研修を5月末に受けたので、この研修から約3ヶ月が経ちました。 しかし、この研修で学んだ、可処分時間を有効活用し、最大限に未来に投資するというのができているかと聞かれると、まだまだできていないと思います。 この記事の執筆にあたり研修の内容を振り返る良い機会になったので、健全な嫉妬心を燃やし頑張ります! サーバー解体 こんにちは、開発本部 DELISH KITCHEN 開発部のきょーです! 僕の方からは GMO ペパボ株式会社様に講義をしていただいた「サーバ解体研修」について紹介していこうと思います。 講義の名前からわかるかもしれませんが、実際にサーバを解体していく内容です!とてもワクワクしますね!僕自身家にサーバもなければ自作 PC を組み立てたこともなかったので、この機会に物理サーバを解体しまくって理解を深めるぞ!と息巻いて講義に臨みました。 解体するぞ!! 参加者は何班かに別れた後、GMO ペパボの社員の方にサポートしていただきながら既に組み立てられているサーバを解体していきます。 最初は ↓ のような状態です。 天板を外すと ↓ のような感じに。ここにびっしり詰まっているそれぞれの部品を解体していいんだ、、、と思うと早く手を動かしたくなりました。 この時点でファンが 4 つ、CPU、メモリ、電源ユニットが 2 つあることがわかるかと思います。電源ユニットが 2 つある理由としては電源の供給元を分散させることによって 1 つの供給元が落ちたとしても稼働し続けられるように、とのことでした!いろんなことが考えられて設計されていることを学びました。(教えてくれたメンバーの方ありがとう!) ここからは班のメンバーで手分けしていきながら解体作業に移っていきます。最初はおっかなびっくり解体していたメンバーも ↓ のようにサーバが解体され尽くされる頃には「これって取れるかな?」と手を動かしていてとても面白かったのを覚えています笑 解体していく!! 解体しました。正直ここまで解体できるとは思っていなかった、、、達成感えぐい、、、(メモリの部分を外した時の写真取り忘れた) また解体しやすいように部品が配置されている設計にとても驚きました。外したいものだけを外せるし、戻したいときもすぐ戻せる、そのような部品の配置、ケーブルの配線になっていて感動しました。(ソフトウェアも交換したい部分だけを交換できるように設計することは大切ですよね。) 一つ一つの部品が組み合わさり PC が完成されていることを学べたとても貴重な機会だったと思います。 元に戻すまでが研修ということで、これから組み立て直していきます。 難しいかと思いましたが、解体しやすかった分元に戻すのも手順さえ覚えておけば簡単でした! 無事に元に戻せました!!おめでたい!! サーバー解体を終えて 解体から元に戻すところまで無事にできました。さてそれでは物理サーバはクラウド技術が発展してきた現代でどのように使われているのでしょうか? 全てクラウドを使ってサービスを運用することは可能です。しかし適切に物理サーバを用いることで運用コストを下げたり、パフォーマンスをあげたり、セキュリティ面を強固にすることができるとのことでした。例えば、クラウドのストレージは格納するデータ量によってコストがかかりますが、物理サーバにそれらを配置することでコストが抑えられるかもしれません。また、機密データ等は物理サーバに配置することでセキュリティの強化を見込めます。 適切にクラウドなのか物理サーバを使うのかを判断できるようになることで考えられる選択肢に幅を持たせられ、エンジニアとして成長できると講師の方が言っていました。クラウドしか触ったことがない自分にとっては実際に分解することで物理サーバの知識や興味を深められ、エンジニアとして成長できる一歩を踏めたと感じています! P.S 自分たちのサービスである DELISH KITCHEN であれば高解像度の動画や画像を取り扱っているので、それらを物理サーバに配置することで運用コストが下がるのではないか、、?と妄想したりしました。もし物理サーバを導入するとした場合、サーバの購入費や、どこにサーバを配置するのか、既存から乗り換えるのにどれくらいコストがかかるのか、など考えなければいけないことがたくさんありそうです。 また、懇親会の時に HDD も解体してきました!!元には戻せませんでした!!!! ISUCON 研修 DELISH KITCHEN 開発部でソフトウェアエンジニアをしている新谷です。私からは第9回のISUCON研修について紹介します。 ISUCON研修では、本番のISUCONのようにチームで与えられた問題に取り組む形式で行われました。 そもそもISUCONとは、「いい感じに スピードアップ コンテスト」の略称で、お題となるWebサービスを決められたレギュレーションの中でどれだけ高速化できるかを競う大会のことです。 事前準備 私はISUCONには参加したことはないのですが、事前にISUCON研修に参加する人たちで有志の勉強会が開かれたのでそれに参加していました。そこでは、ISCUONでよく使われるツールやテクニックについて知ることができ、研修当日に備えることができました。 ツールとしては以下のようなものがあり、研修当日に活用しました。 pt-query-digest slowqueryの解析に使うツール mysqlのログを解析してsqlが遅い順に表示される alp NGINXのアクセスログの解析に使うツール 遅いエンドポイント順に表示される 研修当日 研修当日は、計測ツールの導入、ボトルネックの特定、修正を高速に行いました。計測から修正までの一例としてDBへのインデックス追加を行った際の流れを紹介します。 まず pt-query-digest でslowqueryを解析します。以下は解析結果の一部です。上位に書いているクエリほど遅いクエリです。 # Profile # Rank Query ID Response time Calls R/Call V/M Ite # ==== ============================= ============== ===== ====== ===== === # 1 0x624863D30DAC59FA16849282... 497.2385 69.5% 1885 0.2638 0.02 SELECT comments # 2 0x422390B42D4DD86C7539A5F4... 165.4411 23.1% 2016 0.0821 0.01 SELECT comments # 3 0x100EC8B5C400F34381F9D7F7... 37.9935 5.3% 131 0.2900 0.00 SELECT comments 上記から、 SELECT comments が遅いことがわかりました。そのため、 comments テーブルにインデックスを追加しました。その後再度 pt-query-digest でslowqueryを解析します。 # Profile # Rank Query ID Response time Calls R/Call V/M Item # ==== ============================ ============= ====== ====== ===== ==== # 1 0x4858CF4D8CAA743E839C127... 48.3164 44.0% 933 0.0518 0.00 SELECT posts # 2 0x7A12D0C8F433684C3027353... 10.2537 9.3% 160 0.0641 0.00 SELECT posts # 3 0xDA556F9115773A1A99AA016... 10.1553 9.3% 126417 0.0001 0.00 ADMIN PREPARE # 4 0x396201721CD58410E070DA9... 8.3778 7.6% 58802 0.0001 0.00 SELECT users # 5 0xCDEB1AFF2AE2BE51B2ED5CF... 8.3270 7.6% 191 0.0436 0.00 SELECT comments # 6 0x19759A5557089FD5B718D44... 7.2387 6.6% 15215 0.0005 0.00 SELECT posts # 7 0x624863D30DAC59FA1684928... 4.1172 3.8% 23976 0.0002 0.00 SELECT comments # 8 0x422390B42D4DD86C7539A5F... 3.9942 3.6% 25103 0.0002 0.00 SELECT comments インデックスを追加したことで、 SELECT comments のクエリが大幅に高速化されました。このように、計測してボトルネックを特定し、ピンポイントで修正を行っていきました。上記の流れで、他にもNginxへのキャッシュやクエリのチューニング、N+1問題の解消などを行い、大方目立ったボトルネックを全て解消することができました。 個人的にN+1を時間内に全て解消することができたのは、自分にとって大きな成長だと感じています。今回のISUCONはGo言語を選択したのですが、普段業務でGo言語を書いていることもあり、比較的スムーズにN+1問題を解消することができました。 結果 最終的に、私たちのチーム「唐揚げ丼」は31チーム中2位という結果を残すことができました。 前半にツール導入に手間取ったりして反省点などありますが、ISUCON本番のような空気感を味わうことができ、とても有意義な研修だったと感じています。 ちなみに、1位の黒酢サンドさんのスコアは2,3位のチームのスコアの2倍以上差があり圧倒的でした。後日記事が公開されていましたが、学ぶべき点が多くありました。 【CTO協会研修記録】 未経験エンジニアがISUCONで圧倒優勝するまでの話 特にhtmlファイルのExecuteが遅かったため、htmlを全てバイト列で持ってレスポンスに書き込むといった点にはかなり驚きました。 研修全体を通して この研修を通して私たちが得たものは大きく分けて 2 つあると思っています。 1 つ目はエンジニアとして成長するためのきっかけです。 社内で利用しているような技術から触ったことがない技術まで本当に幅広く体験することができました。また、成長するためのキャリアについての考え方も学ぶことができました。ただ学んで終わりにするのではなく、学んだ技術を自分のものにし、どう会社に還元していくかを考え行動することが次のアクションとしてあると思います。そして研修を通していただいた分を何らかの形でこの業界に還元していきたいです! 2 つ目は研修を一緒に駆け抜けてくれた仲間です。 同じ年代の方達と会社の枠を超えて巡り会えたのは一番の財産だと思っています。ここで知り合った仲間とのコミュニティを大事にし、健全な嫉妬を与えたり、受けたりしてこれからも切磋琢磨していきたいです!最初はよそよそしかったですが、研修を通して仲を深め、今では輪読会を行うようにまでなりました! この研修を開いてくださった日本 CTO 協会の方々と、講義をしてくださった企業様、スポンサーをしていただいた企業様には本当に感謝しています。ありがとうございました!!
はじめに こんにちは、Retail Hub 事業部でエンジニアを務めている 羽馬 です。 この記事は、Vue.js 日本ユーザーグループ主催の Vue.js v-tokyo Meetup #21 で登壇した際の発表資料を元に、VueUse というライブラリを使って Vue.js 開発を効率化する方法をご紹介します。 登壇資料はこちら: speakerdeck.com VueUse とは VueUseは、Vue Composition APIのための包括的なユーティリティコレクションです。以下の特徴があります: 200以上の便利な関数を提供 Vue.jsアプリケーション開発の生産性を大幅に向上 ローカルストレージ、デバイス情報、スクロール位置、フォームバリデーションなど、幅広い機能をカバー 宣言的で再利用可能なコンポーザブル ライフサイクルフックの自動処理 必要な機能のみをインポート可能(tree-shaking対応) 高度にカスタマイズ可能なオプション vueuse.org VueUseを使用することで、複雑な機能を簡潔に実装でき、開発効率を劇的に向上させることができます。 VueUseが提供する主な機能 VueUseは非常に多くの機能を提供していますが、ここでは特に有用な機能をいくつか紹介します: 状態管理 useStorage : ローカルストレージやセッションストレージとリアクティブな状態を同期 useState : シンプルな状態管理 センサーと端末情報 useMouse : マウスの位置を追跡 useGeolocation : デバイスの位置情報を取得 useDeviceOrientation : デバイスの向きを検出 ブラウザ操作 useClipboard : クリップボードの操作 useDark : ダークモードの切り替え useFullscreen : フルスクリーンモードの制御 アニメーションとタイミング useInterval : 定期的な処理の実行 useTimeout : 遅延処理の実行 useTransition : スムーズな状態遷移 ネットワークとAPI useFetch : HTTPリクエストの簡易化 useWebSocket : WebSocketの操作 UI操作 useVModel : v-modelの簡易実装 useInfiniteScroll : 無限スクロールの実装 これらの機能を使用することで、一般的なWeb開発タスクを簡単に、そして効率的に実装することができます。 VueUseの具体的な使用例 VueUseの機能の中から、特に有用なものをいくつか抜粋して具体的に説明します: ローカルストレージの利用 ( useStorage ) ローカルストレージの利用 ( useStorage ) import { useStorage } from '@vueuse/core' const state = useStorage ( 'my-storage-key' , { count : 0 }) // stateを変更すると自動的にローカルストレージに保存される state . value . count ++ VueUseを使用しない場合: import { ref , watch } from 'vue' const state = ref ( JSON . parse ( localStorage . getItem ( 'my-storage-key' )) || { count : 0 }) watch ( state , ( newState ) => { localStorage . setItem ( 'my-storage-key' , JSON . stringify ( newState )) } , { deep : true }) // stateを変更する度に手動でwatchを設定する必要がある state . value . count ++ ダークモードの実装 ( useDark ) VueUseを使用する場合: import { useDark , useToggle } from '@vueuse/core' const isDark = useDark () const toggleDark = useToggle ( isDark ) // ダークモードの切り替えが簡単 toggleDark () VueUseを使用しない場合: import { ref , watch } from 'vue' const isDark = ref ( false ) const toggleDark = () => { isDark . value = ! isDark . value if ( isDark . value ) { document . documentElement . classList . add ( 'dark' ) } else { document . documentElement . classList . remove ( 'dark' ) } } watch ( isDark , ( newValue ) => { localStorage . setItem ( 'dark-mode' , newValue ) }) // 初期化時にローカルストレージから設定を読み込む isDark . value = JSON . parse ( localStorage . getItem ( 'dark-mode' ) || 'false' ) 無限スクロールの実装 ( useInfiniteScroll ) VueUseを使用する場合: import { useInfiniteScroll } from '@vueuse/core' const el = ref ( null ) const { arrivedState , reload } = useInfiniteScroll ( el , () => { // 新しいデータをロードする処理 }) VueUseを使用しない場合: import { ref , onMounted , onUnmounted } from 'vue' const el = ref ( null ) const isLoading = ref ( false ) const checkScroll = () => { if ( ! el . value ) return const { scrollTop , scrollHeight , clientHeight } = el . value if ( scrollTop + clientHeight >= scrollHeight - 50 && ! isLoading . value ) { isLoading . value = true // 新しいデータをロードする処理 // 処理が完了したらisLoading.value = falseにする } } onMounted (() => { el . value ?. addEventListener ( 'scroll' , checkScroll ) }) onUnmounted (() => { el . value ?. removeEventListener ( 'scroll' , checkScroll ) }) これらの例から分かるように、VueUseを使用することで、複雑な機能をより簡潔に、そして宣言的に実装できます。特に、ローカルストレージの同期やダークモードの実装など、頻繁に必要となる機能を簡単に実装できる点が大きな利点です。 一方で、VueUseを使用しない場合、より多くのボイラープレートコードが必要となり、ライフサイクルフックの管理やイベントリスナーの設定など、細かい実装の詳細に注意を払う必要があります。 ただし、プロジェクトの規模や要件によっては、外部ライブラリに依存せず、Vueの基本機能のみで実装することが適切な場合もあります。特に、使用する機能が限定的で、バンドルサイズを最小限に抑えたい場合などは、VueUseを使用しない選択肢も考慮に値します。 まとめ VueUseは、Vue.jsアプリケーション開発において非常に強力なツールです。以下に、VueUseを使用することの主な利点をまとめます: 開発効率の向上 : 複雑な機能を少ないコードで実装可能 頻繁に使用される機能が既に最適化されて提供されている コードの可読性と保守性の向上 : 宣言的なAPIにより、コードの意図が明確になる コンポーザブルの再利用が容易 学習曲線の緩和 : Vue.jsのベストプラクティスが組み込まれている 豊富なドキュメントとコミュニティサポート パフォーマンスの最適化 : ライフサイクルフックの自動管理 tree-shakingに対応し、必要な機能のみをバンドル可能 Vue.jsエコシステム互換性 : Vue.js 2、Vue 3、Nuxt.jsなど、様々な環境で使用可能 VueUseを活用することで、開発者はビジネスロジックの実装に集中でき、結果としてより高品質なアプリケーションを効率的に開発することが可能になります。ただし、プロジェクトの要件や規模に応じて、VueUseの使用を検討することが重要です。 今後のVue.js開発において、VueUseは重要なツールの一つとなることが期待されます。継続的な学習と実践を通じて、VueUseの可能性を最大限に活用していくことをお勧めします。