株式会社RevCommのブログ - TECH PLAY

TECH PLAY

株式会社RevComm

株式会社RevComm の技術ブログ

185

Hi, Jose here! I recently began developing a private git package to be used by many services from our organization. While the basic setup was relatively straightforward, I quickly realized how scaling it encompassed many concepts. Factors like integrating with various repositories and adapting your CI/CD pipelines can significantly raise the bar. In this blog post, I will walk you through the process of installing a private git dependency and demonstrate how to use Poetry effectively to manage packages from multiple code repositories. Requirements Python 3.10 Poetry version 1.8.4 Working with public git dependencies Installing a dependency in Poetry is simple enough. Just run poetry add package_name . This will add the respective package to the pyproject.toml file. For git dependencies, we must specify the location of the repository with the git key. Let’s install the requests library from Github. poetry add git + https: //github.com/psf/requests.git@v2.32.2 Now your pyproject.toml will look as follows requests = { git = "https://github.com/psf/requests.git" , rev = "v2.32.2" } If you don’ specify the rev property, Poetry will take up the latest commit of the main branch. Check the official docs for more information. Installing a private git dependency Poetry needs to authenticate to your git provider to install private dependencies. In the case of Github, we create a Personal Access Token (PAT). A Personal Access Token provides a secure way to authenticate to GitHub without the need of a password. Generate a PAT, set up authentication and install the package. $ poetry config repositories . git - org - project https : //github.com/org/private_lib.git $ poetry config http - basic . git - org - project username $PAT_TOKEN $ poetry add git + https: //github.com/org/private_lib.git Notice that the pyproject.toml looks almost the same as the public repo. Poetry ensures that your private credentials aren’t reflected. [ tool . poetry . dependencies ] python = ">=3.10, <3.13" requests = { git = "https://github.com/psf/requests.git" , rev = "v2.32.2" } pydantic = "^2.10.3" private_lib = { git = "https://github.com/org/private_lib.git" } Managing multiple private repositories. Now imagine your project needs your sales team’ internal libraries hosted in a private GitHub repository, but your research team maintains their libraries in an AWS CodeArtifact repository. How would you seamlessly integrate both? Enter sources . Poetry uses sources to discover and install packages in your project. The default one is PyPI. Sources enable seamless integration of internal, third-party libraries without disrupting the main dependency flow. In the example above, we would run $ poetry source add -- priority = supplemental research https : //domain.d.codeartifact.ap-northeast-1.amazonaws.com/pypi/research/ $ poetry source add -- priority = supplemental sales https : //github.com/org/sales.git Poetry will add the following to your pyproject.toml . [[ tool . poetry . source ]] name = "research" url = "https://domain.d.codeartifact.ap-northeast-1.amazonaws.com/pypi/research/" priority = "supplemental" [[ tool . poetry . source ]] name = "sales" url = "https://github.com/org/sales.git" priority = "supplemental" We use priority supplemental to tell Poetry that PyPI should still be the main code repository. You can tweak the priorities to your needs, for instance, you can disable PyPI completely . Remember that we still need to setup authentication for each source. $ poetry config repositories . research https : //domain.d.codeartifact.ap-northeast-1.amazonaws.com/pypi/research/ $ poetry config http - basic . research username ca_token $ poetry config repositories . sales https : //github.com/org/sales.git $ poetry config http - basic . sales username token Finally we install our libraries $ poetry add sales - lib -- source sales $ poetry add research - lib -- source research Conclusion In this blog post we reviewed the steps to add private git repositories into Poetry. We also looked at how to manage multiple code repositories with Poetry. In a production environment, we would containerize the application and integrate it to a CI/CD pipeline. Those steps, although similar in nature, require extra care specially when using secret tokens. References https://python-poetry.org/docs/dependency-specification/#git-dependencies https://python-poetry.org/docs/repositories/#package-sources https://medium.com/@irac.grgic/poetry-automatically-configure-credentials-for-all-private-repositories-541ce3b78759
はじめに RevComm のフロントエンドエンジニアの上川です。 MiiTel Call Center というプロダクトの開発を担当しています。 これまで、ロードマップ機能の開発では、バックエンドとフロントエンドの担当者が完全に分かれていました。 今回は、フロントエンドを担当してきた自分が、バックエンド開発に挑戦してみた経験と、そこから得た学びについて共有したいと思います。 バックエンド開発に挑戦した背景 バックエンドチームが他のタスクに注力している状況を受け、コールセンターの「ロードマップ機能」をフロントエンドチームだけで実装する提案がありました。 以前からバックエンド開発に興味があったため、この機会に挑戦することを決めました。 実装内容 コールセンターに表示する新しい項目の実装を担当しました。 具体的には、新しい項目の値をバックエンドで計算・取得し、フロントエンド側のテーブルに表示するまでの一連の実装を行いました。 この経験により、これまでのフロントエンド中心の視点から、データ取得からUI表示までの全体的なフローを把握できるようになりました。 開発の進め方 PMの主導のもと、以下のように段階的に進めていきました。余裕を持った計画のおかげで、不安なく円滑に進行することができました。 最初の1ヶ月は、Design Docsを活用しながら週1回の短いミーティングで要件を詰めていきました。 Design Docsには以下の内容を記載していきました。 項目の値の計算式 既存の類似項目の実装調査と修正箇所の特定 Redashを活用したSQL文の作成 次に、他のフロントエンドメンバーとペアで1つの機能を実装し、それぞれの担当部分をミーティングで共有しました。 最後に、1つの機能を単独で実装しました。 チームサポートのありがたさ ENUMを扱うマイグレーションやAWS Athenaでのテーブル再構成など、初めて触れる領域で苦労することもありました。 ただし、バックエンドチームのメンバーにSlackやMeetで気軽に質問できたおかげで、エラーや不明点をスムーズに解決できました。 バックエンドとフロントエンドが同じスクラムチームで活動していることや、チームオフサイトで直接交流する機会があったことで、質問しやすい環境が自然とできていたと感じています。 新たな学び Python 以前は基礎的な経験しかなかったPythonですが、今回文法を体系的に学び直し、Pydanticなどのライブラリの役割についても深く理解することができました。 GraphQL QueryやSubscriptionの仕組みについて、データベースからデータを取得し、レスポンスとして返すまでの一連の流れを理解することができました。 DB データベースマイグレーションの実践的な手法と重要な注意点について学びました。 AWS AthenaのテーブルとECSのAuto Scalingの基本的な操作を経験しました。 エラー対応 バックエンドの仕組みを理解したことで、Datadogから通知されるエラーの内容や影響範囲を正確に把握できるようになりました。 レポートのバッチ処理でエラーが発生した際も、どのデータが欠損する可能性があるのか、そしてそれがフロントエンドの表示にどう影響するのかを的確に判断できるようになりました。 ロードマップ機能を一人で実装できるようになった 今回の最大の成果は、 フロントエンドからバックエンドまで、ロードマップ機能を一人で実装できるようになった点 です。 これまで他のメンバーに頼っていた領域も自力で対応できるようになり、開発の効率性と柔軟性が大幅に向上しました。 この経験を糧に、今後はより大規模な機能開発にも挑戦していきたいです。 技術的な視野が広がったことで、複雑な課題にも自信を持って取り組めるようになりました。 おわりに 今回、バックエンド開発に挑戦したことで、新たなスキルを身につけることができました。これまでブラックボックスだった部分が明確になり、フロントエンドとバックエンドの仕組みを深く理解できるようになりました。 バックエンドメンバーやチームのサポートのおかげで、未知の領域や初歩的な課題を着実に乗り越え、自走できる範囲が広がりました。その結果、ロードマップ機能をフロントエンドからバックエンドまで一人で実装できるようになり、開発の効率性と柔軟性が向上し、自身の成長も実感できました。 今後は、この経験で得た知見とスキルを活かし、より大規模で複雑な機能開発にも積極的に挑戦していきます。さらなる学びを重ね、チームとプロダクトに貢献できるエンジニアとして成長を続けていきたいと思います。
はじめに vinxi はフルスタックアプリケーションやメタフレームワークの構築が可能なパッケージです。 開発サーバーとバンドラーに Vite を、本番サーバーには Nitro を使用しています。 SolidStart や TanstackStart で採用されています。 今回は vinxi を使ってメタフレームワークを作ってみます。 進めるにあたり、公式のサンプルや以下の記事を参考にさせていただきました。 Bullding a React Metaframework with Vinxi Simple RSC With Vinxi セットアップ package.json を作成し、 npm init -y 以下の内容を追加します。 { " name ": " try-vinxi ", " version ": " 1.0.0 ", " description ": "", " type ": " module ", " scripts ": { " dev ": " vinxi dev ", " build ": " vinxi build ", " preview ": " vinxi preview " } , " keywords ": [] , " author ": "", " license ": " ISC ", " dependencies ": { " @vinxi/react ": " ^0.2.5 ", " @vitejs/plugin-react ": " ^4.3.4 ", " react ": " ^18.3.1 ", " react-dom ": " ^18.3.1 ", " vinxi ": " ^0.5.0 " } , " devDependencies ": { " @types/react ": " ^18.3.12 ", " @types/react-dom ": " ^18.3.1 ", " typescript ": " ^5.7.2 " } } tsconfig.json を作成します。 { " compilerOptions ": { " module ": " ESNext ", " moduleResolution ": " bundler ", " jsx ": " react-jsx ", " esModuleInterop ": true , " strict ": true } } 依存関係をインストールします。 npm install SPA まずはシンプルな SPA モードを作成します。 index.ts を作成 ルートに index.ts を作成します。空の HTML を返すだけのハンドラーです。 import { eventHandler } from 'vinxi/http' ; export default eventHandler(() => { return new Response ( `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vinxi</title> </head> <body> <div id="root"></div> <script src="./src/entry-client.tsx" type="module"></script> </body> </html>` , { status : 200 , headers : { 'Content-Type' : 'text/html' , } , } ); } ); entry-client.tsx を作成 src ディレクトリを作成し、その中に entry-client.tsx を作成します。 import { createRoot } from 'react-dom/client' ; import Counter from './counter' ; createRoot( document . getElementById ( 'root' )!).render( < Counter /> ); counter.tsx を作成します。 import { useState } from 'react' ; export default function Counter () { const [ count , setCount ] = useState( 0 ); return ( < div > < h1 > Counter </ h1 > < p > { count } </ p > < button onClick = { () => setCount(count + 1 ) } > Increment </ button > </ div > ); } app.config.ts を作成 ルートに app.config.ts を作成し、 createApp 内に vinxi の設定を記述します。 @vitejs/plugin-react を追加します。vinxi は Vite を使用しているため、Vite のプラグインをそのまま使用できます。 npm install @vitejs/plugin-react spa ルーターを追加します。handler には先ほど作成した index.ts を指定します。 import { createApp } from 'vinxi' ; import pluginReact from '@vitejs/plugin-react' ; export default createApp( { routers : [ { name : 'spa' , type : 'spa' , handler : './index.ts' , target : 'browser' , plugins : () => [ pluginReact() ] , } , ] , } ); 開発サーバーを起動します。 npm run dev カウンターが表示されることを確認します 🎉 SSR 次に SSR モードを作成します。 app.config.ts を編集 まずは app.config.ts を編集します。 spa ルーターを削除し、 client と ssr ルーターを追加します。 import { createApp } from 'vinxi' ; import pluginReact from '@vitejs/plugin-react' ; export default createApp( { routers : [ { name : 'client' , type : 'client' , handler : './src/entry-client.tsx' , target : 'browser' , plugins : () => [ pluginReact() ] , base : '/_build' , } , { name : 'ssr' , type : 'http' , handler : './src/entry-server.tsx' , target : 'server' , plugins : () => [ pluginReact() ] , } , ] , } ); 各ファイルを作成していきます。 app.tsx を作成 src ディレクトリに app.tsx を作成します。 createAssets は各アセットを注入するコンポーネントを作成します。遅延コンポーネントになっているため、 Suspense でラップします。 import { getManifest } from 'vinxi/manifest' ; import { createAssets } from '@vinxi/react' ; import { Suspense } from 'react' ; import Counter from './counter' ; const Assets = createAssets( getManifest( 'client' ).handler, getManifest( 'client' ) ); export default function App () { return ( < html > < head > < Suspense > < Assets /> </ Suspense > </ head > < body > < div id = "root" > < Counter /> </ div > </ body > </ html > ); } entry-client.tsx を編集 createRoot を hydrateRoot に変更します。 クライアントのランタイムを読み込むために vinxi/client をインポートします。 import { hydrateRoot } from 'react-dom/client' ; import App from './app' ; import 'vinxi/client' ; hydrateRoot( document , < App /> ); entry-server.tsx を作成 src ディレクトリに entry-server.tsx を作成します。 import { getManifest } from 'vinxi/manifest' ; import { eventHandler } from 'vinxi/http' ; import { renderToPipeableStream } from 'react-dom/server' ; import App from './app' ; export default eventHandler( { handler : async ( event ) => { const clientManifest = getManifest( 'client' ); const stream = await new Promise ( async ( resolve ) => { const stream = renderToPipeableStream( < App /> , { onShellReady () { resolve(stream); } , bootstrapModules : [ clientManifest.inputs[clientManifest.handler ] .output.path, ], bootstrapScriptContent : `window.manifest = ${ JSON . stringify ( await clientManifest.json() ) } ` , } ); } ); event.node.res.setHeader( 'Content-Type' , 'text/html' ); return stream; } , } ); 開発サーバーを起動します。 npm run dev SSR された HTML が返ってきました 🎉 File system routing vinxi はファイルシステムルーティングを作成するための機能を提供しています。 FileSystemRouter を作成 vinxi の BaseFileSystemRouter を継承した FileSystemRouter を作成します。今回は app.config.ts の中に書いていきます。 toPath メソッドは引数にファイルのパスを受け取り、ルートのパスを返します。 cleanPath はディレクトリ名と拡張子を取り除く vinxi のユーティリティ関数です。 toRoute メソッドは引数にファイルのパスを受け取り、ルートオブジェクトを返します。このルートオブジェクトは vinxi/routes モジュールによってアプリケーションに提供されます。 class FileSystemRouter extends BaseFileSystemRouter { toPath ( src : string ) { const routePath = cleanPath(src, this .config) . slice ( 1 ) . replace ( /index$/ , '' ); return routePath?. length > 0 ? `/ ${ routePath } ` : '/' ; } toRoute ( filePath : string ) { return { path : this .toPath(filePath), $component : { src : filePath, pick : [ 'default' ] , } , } ; } } 各ルーターの routes プロパティに FileSystemRouter を追加します。最終的に app.config.ts は以下のようになります。 import { createApp } from 'vinxi' ; import pluginReact from '@vitejs/plugin-react' ; import { BaseFileSystemRouter, cleanPath } from 'vinxi/fs-router' ; import path from 'node:path' ; class FileSystemRouter extends BaseFileSystemRouter { toPath ( src : string ) { const routePath = cleanPath(src, this .config) . slice ( 1 ) . replace ( /index$/ , '' ); return routePath?. length > 0 ? `/ ${ routePath } ` : '/' ; } toRoute ( filePath : string ) { return { path : this .toPath(filePath), $component : { src : filePath, pick : [ 'default' ] , } , } ; } } export default createApp( { routers : [ { name : 'client' , type : 'client' , handler : './src/entry-client.tsx' , target : 'browser' , plugins : () => [ pluginReact() ] , base : '/_build' , routes : ( router , app ) => { return new FileSystemRouter( { dir : path. join (__dirname, 'src/routes' ), extensions : [ 'tsx' , 'ts' ] , } , router, app ); } , } , { name : 'ssr' , type : 'http' , handler : './src/entry-server.tsx' , target : 'server' , plugins : () => [ pluginReact() ] , routes : ( router , app ) => { return new FileSystemRouter( { dir : path. join (__dirname, 'src/routes' ), extensions : [ 'tsx' , 'ts' ] , } , router, app ); } , } , ] , } ); 各ルートファイルを作成 先ほど指定した src/routes ディレクトリに、いくつかルートファイルを作成します。 src/routes/index.tsx import Counter from '../counter' ; export default function Index () { return ( < div > < h1 > Index </ h1 > < Counter /> </ div > ); } src/routes/about.tsx export default function About () { return ( < div > < h1 > About </ h1 > < p > This is the about page. </ p > </ div > ); } src/routes/docs/guide.tsx export default function Guide () { return ( < div > < h1 > Guide </ h1 > < p > This is the guide page. </ p > </ div > ); } React Router をインストール 今回はルーターライブラリに React Router v6 を使用します。 npm install react-router-dom@6 app.tsx を編集 import { getManifest } from 'vinxi/manifest' ; import { createAssets, lazyRoute } from '@vinxi/react' ; import { Suspense } from 'react' ; import fileRoutes from 'vinxi/routes' ; import { Route, Routes } from 'react-router-dom' ; const clientManifest = getManifest( 'client' ); const ssrManifest = getManifest( 'ssr' ); const routes = fileRoutes. map (( route ) => ( { ...route, component : lazyRoute(route.$component, clientManifest, ssrManifest), } )); const Assets = createAssets(clientManifest.handler, clientManifest); export default function App () { return ( < html > < head > < Suspense > < Assets /> </ Suspense > </ head > < body > < div id = "root" > < Suspense > < Routes > { routes. map (( route ) => ( < Route key = { route.path } path = { route.path } element = { < route . component /> } /> )) } </ Routes > </ Suspense > </ div > </ body > </ html > ); } entry-server.tsx を編集 App コンポーネントを StaticRouter でラップし、 event.path を location に渡します。 <StaticRouter location = { event . path } > < App /> </StaticRouter> entry-client.tsx を編集 App コンポーネントを BrowserRouter でラップします。 hydrateRoot( document , < BrowserRouter > < App /> </ BrowserRouter > ); index.tsx にリンクを追加 export default function Index () { return ( < div > < h1 > Index </ h1 > < Counter /> < Link to = "/about" > About </ Link > < Link to = "/docs/guide" > Docs </ Link > </ div > ); } 開発サーバーを起動します。 npm run dev リンクから各ページに遷移できることを確認します 🎉 おわりに 今回は vinxi を使って SSR とファイルシステムルーティングを備えたメタフレームワークを作成しました。 他にもミドルウェアや Server functions など、vinxi には様々な機能があります。 自分だけのメタフレームワークを作ってみるのも楽しそうですね。
この記事は RevComm Advent Calendar 2024 の 9 日目の記事です はじめに RevComm では Github を使用して開発を行っており、コードレビューの依頼も PR で行われます。Github の PR の画面では変更差分の表示ができるのでレビューにも使えるのですが、個人的にはエディタを使用してレビューするのがお勧めです。 この記事では、エディタを使用してレビューすることのメリットをご紹介します。 エディタでのレビューとは この記事におけるエディタでのレビューとは、エディタで変更差分を表示することを意味します。VS Code など最近のエディタでは拡張機能をインストールすることで、エディタ上で変更差分を表示することが可能です。 ちなみに私は Neovim を使っており、octo.nvim という拡張 (を 自分で改造したもの ) でレビューを行っています。Neovim 上でレビューコメントも追加・編集ができてとても便利です。 エディタでレビューすることのメリット Github 上でのレビューに対して、エディタでレビューすることのメリットをいくつか挙げます。 普段使い慣れているエディタを使用することで様々な操作をスムーズに行える (Vim を使っている人は特に) ブラウザ内検索に比べて、よりリッチな検索を行える 静的解析がおかしな点 (不要なライブラリのインポートや、スペルミスなど) を見つけてくれる これらのメリットのほかに、レビュー観点から見たメリットもあります。 以降ではレビュー観点から見たエディタを使用するメリットについて説明します。 レビューの観点から見たエディタを使用するメリット レビュー観点は様々あると思うのですが、私はレビューを行う際、コードを縦と横に見ることを意識しています。 縦に見る 縦に見るとは、変更箇所の前後を見ることです。 個人的な感想ですが、レビューで変更箇所単体に大きな問題が見つかることはほとんどないです。問題がある場合の大半は、変更箇所の前後の処理との組み合わせにあります。 説明のために以下の例を見てみます。 x: str = "Nanika no atai" # 既に定義されている変数 use_x1(x) # X を使用している箇所 + #### ここから変更箇所 ################## + # x を特定のフォーマットの文字列に変換 + x = format_to_xxx(x) + #### ここまで変更箇所 ################## use_x2(x) # X を使用している箇所 この例では、変数 x の文字列を特定のフォーマットに変換する変更を入れており、フォーマット単体ではおかしな点はありません。 しかし、この変更箇所の前後では変数 x を使用しています ( use_x1 と use_x2 の関数)。もし、 use_x2 がフォーマット前の値を期待していたらどうなるでしょうか?逆に use_x1 もフォーマット後の値を期待していた場合はどうでしょうか?このような場合、変更したコードをそれぞれの関数の呼び出し前後に移動する必要があるでしょう。 この例の場合、縦に見るとは x を変更した箇所の前後で x を使用している部分に注目するということです。 このように縦にコードを見るには、Github の PR 画面では少し大変です。Github の画面では変更箇所の前後の行はデフォルトで非表示になっているからです。これらを表示するには画面を何度もクリックする必要があります。エディタを使う場合、前後の処理を確認するのに特別な手順を必要ありません。この観点からすると、縦に見るにはエディタを使うと非常に効率的です。 横に見る 縦の次は横ですが、これは簡単に言うと変更したファイル以外のファイルも見ることです。 具体的な例をいくつか挙げます。 変更した関数等を参照している全ての箇所で、変更内容がどのような影響を与えているか確認する 変更対象と類似したコードが存在していないか確認し、存在している場合はそちらにも同様の変更が必要か確認する エディタを使用してこれらの観点でレビューを行う場合、LSP を使用して参照箇所にコードジャンプしたり、grep 相当の検索でリポジトリ内を検索すると効率的に行えます。 Github でも参照している箇所を一覧化してくれたりはしますが、エディタを使用した方が少ない手間でコードジャンプが可能です。 まとめ コードレビューにエディタを使用するメリットをレビューの観点から紹介しました。 ブラウザ上でも Github Codespaces を使うとブラウザ上で VS Code を開き、レビューをすることもできます。しかし、普段開発で使用している設定・拡張を用意する手間を考えると、手元のエディタでレビューをすることが個人的にはおすすめです。 エディタを利用することでより効率的にレビューすることが可能になると思いますので、ぜひ一度試してみてください。
はじめに Phone Divという部署でBackendを担当している中島です。 RevCommではMiiTelをはじめ、複数のマイクロサービスの外形監視をチームごとに行う必要があります。 今回は DataDog Synthetic Testing を利用したE2Eテスト・外形監視の実装、その運用について、知っていることをまとめました。テストの作り方から、Autifyとの違いなどにも触れてみたいと思います。 想定読者 DataDogによるE2Eテスト・外形監視に興味のある方 フロントエンド・バックエンド開発者 システムの保守担当者 Quality Assurance(QA)チームの方 DataDog Synthetic Testingを使ってみる 表題のE2Eテストや外形監視の作成は、DataDogの中の「 Synthetic Monitoring & Testing 」というサービスで利用可能です。 API Test を作る 「 API Test 」または「 Multistep API Test 」で作成できます。 API Testの方は一つのAPIに対するリクエストをテストする機能で、Multistep API Testの方は複数のAPIを連続的にテストする機能です。 それぞれ、「HTTP」だけでなく「gRPC」「SSL」「DNS」「WebSocket」「TCP」「UDP」「ICMP」などのプロトコルにも対応しているため、非常に幅広い用途で利用できることが分かります。 Multistep API Testは、例えば、最初のステップでHTTPリクエストを使って認証を通し、BearerなりJWTなりを取得して次のステップ以降でそれを用いてリクエストするなど、複数のAPIを利用するテストシナリオを検証するケースに対応しています。 「 Locations 」というタブからサーバの実行環境は世界中のクラウドのうち、どこから実行できるか選択できます。世界的にサービスを展開している場合に、海外からだと挙動が変わる機能があっても、しっかりテストできることが分かります。 ただ、複数選択した場合、それらのテストは並列で実行され、それぞれのテスト実行がコストに繋がりますので、様々な地理的拠点から見え方が同じであるなら、どれか一つお好きな実行環境を選ぶことになると思います。 「 Assertions 」というタブから豊富なAssertion機能を設定できます。status codeの検証はもちろん、response body中の要素をjson pathを指定して取得・確認したり、最近実装されたJavaScript Assertion機能ではChaiを用いたAssertionを埋め込むこともできたりするようです。 Browser Test を作る ブラウザのE2Eテストを作成する場合、「 Browser Test 」を選択します。E2Eテスト作成機能は「 Edit Test 」「 Edit Recording 」「 View Results 」の3つのタブから構成されます。 Edit Test 「 Edit Test 」ではE2Eテストの実行環境や実行頻度などを設定できます。 「 Starting URL 」にテストシナリオ開始時に利用するURLを指定します。この「Starting URL」を設定できる点が非常に便利で、一度テストを作ってしまえば、環境(開発環境、ステージング環境)ごとにこのURLを差し替えるだけでその環境でも同様のテストも動くようになります。これによって環境ごとにテストを用意する手間が省け、効率的にテストを作ることができます。 「 Browser & Devices 」でブラウザについてはChrome、Edge、Firefoxの3つから、デバイスについてはLaptop、Tablet、Mobileから実行環境を選べます。 「 Scheduling & Alert Conditions 」では実行間隔を設定でき、最短 5分・最長 1週間で選択することになります。 Edit Recording 「 Edit Recording 」では実際にテストする画面を触りながらテストを組み立てていきます。CSVやPDFなどファイルをフォームに設定するテストなども作成できます。 ただ、ブラウザのマイクやカメラに許可を与えるようにテストは正常に動作しなかったので、この点が少し残念な点でした。(2024年12月現在) ブラウザテストのAssertionsではHTML要素を指定した結構細かいAssertや、特定の文字列がページに含まれるか、ファイルがダウンロードされるか、なども利用できます。 例えば、以下のようなAssertができることは確認しています。 指定したHTML要素が存在する 指定したHTML要素の状態(〜が含まれる、〜から始まる、など細かく指定可能)が正しい 画面上に「XXX」という文字列が含まれる 指定したラジオボタン、チェックボックスがchecked / uncheckedになっている View Results 「 View Results 」では作成したテストの結果を見ることができます。 過去の実行結果やFailした場合はどこのAssertionがコケたのかを確認できる 各ステップごとの実行時間(Duration)、CLS(Cumulative Layout Shift)やLCP(Largest Contentful Paint)も確認することができる 結果を見る システムの状態を一目で確認できるようにチームに関係するE2Eテストをダッシュボードにして、運用することにしました。 Widgetを自分で作成することで好きな統計が載せられる 私のチームではCLS、LCPを自作して追加しました。 ブラウザ・環境・タグごとのテスト成功 / 失敗回数を一目で確認できる 実行時間、CLS、LCPに時系列で変化があるかどうか確認できる 通知をする 基本的にDataDogのMonitorと同じ機能でチャットサービスへと通知を飛ばすことができます。 今回はE2Eテストが失敗したらチームのSlack Channelに通知に飛ばすようにしました。 料金について 料金はAPIテストとブラウザテストによって異なります。以下のページを参考にしながら見積もりできるはずです。 www.datadoghq.com 注意点として、Synthetic Testingは25ステップ以内のシナリオを1回と見做し、25ステップを超えると2回テスト実行したと見做されるようです。 これを踏まえた上で、25ステップ以内テストシナリオ数を n 、1日何回実行するかを t とし、1ヶ月30日とすると以下の計算式でコストを見積もると以下のようになります。 monthly_cost =(n * t / 1000)* c * 30日 n: テストシナリオ数 t: 1日何回実行するか c: 1000回あたりの価格。オンデマンド使用料が目安 例えば、1日1回 5シナリオ実行した場合はひと月あたりカフェのコーヒーくらいの料金になるでしょう。 Datadog Synthetic Testingの強み・弱み E2Eテストといえば Autify が思い当たる方が多いと思いますが、2024年12月現在の調査結果に基づいてDataDog Synthetic Testingとの違いを紹介してみようと思います。 DataDogでは以下のことができます。 APIテストができる あくまで私が触ってみた感触にはなりますが、DataDog Synthetic TestingはHTMLの要素やAPIまで検証したい開発者向き、Autifyは外部品質を担保したい組織向きの機能が充実しているものと思っています。DataDogのAssert機能の豊富さもそれゆえなのかもしれません。 毎回のテスト実行で LCP、CLS など Core Web Vital が算出できる 実行環境を細かく指定できる 本記事の「Locations」画面を参照 Autifyでは以下のことができます。 E2Eテスト時のブラウザにデバイス使用許可を与えることができる ブラウザにデバイス使用許可(マイク、カメラなど)を与えられるかどうか大きな違いでした。MiiTelのIP電話はマイクの使用が不可欠なサービスなので、DataDogで架電のE2Eテストが作れなかったのが残念でした。ただ、Autifyでも音声を入れたり動画を映したりすることはできないのでその点は注意してください。 作成したテストシナリオを編集する際に、途中の画面から編集できる DataDogでブラウザテストのシナリオを修正したい場合、操作を一からやり直すことになります。一方Autifyでは、シナリオの途中から編集することができます。 テストシナリオを複数組み合わせで実行できる DataDogでは作ったテストシナリオのCloneはできるんですが、作ったシナリオを柔軟に組み合わせて使うことができないようです。 この辺りはDataDog、Autifyの強み・弱みがあるので、やりたいことに応じてツールを使い分けるのが良いと思いました。 感想 私個人としてはE2Eテスト実行のついでにCore Web Vitalが算出できるのが優れていると感じました。Webサービスはユーザやデータが増加するにつれて動作が遅くなることがあり、気がついた頃には原因が根深くなってしまいがちです。 DataDog Synthetic Testingでは作成したE2Eテストのダッシュボードを通して時系列でCore Web Vitalが確認できるので、外形監視には持ってこいのサービスだと思いました。 導入を検討される場合、本記事が参考になれば幸いです。
はじめに 最近、Reactに useEffectEvent という実験的APIが存在することを知りました。 弊社で提供しているMiiTel Phoneにおいては、WebSocketやWebRTCなどによってさまざまなタイミングや箇所で非同期的にイベントが発生します。 その関係もあって useEffect を広く活用しているのですが、そういった処理をこの useEffectEvent を使うことによって単純化できるのではないかと思い、調べてみることにしました。 注意 useEffectEvent は現状では実験的APIです。安定版のReactでは利用することができません。もし試してみたい場合は、下記パッケージの experimental バージョンが必要です: react@experimental react-dom@experimental eslint-plugin-react-hooks@experimental 活用例 まず useEffectEvent の活用例として、React Routerが Location の変更を検知するたびに特定のコールバック関数を実行するようなケースを元に考えてみます。 useEffectEvent を使わない場合 useEffectEvent を使わない場合、意図通りに動作させるためには、以下のように useRef や useInsertionEffect などを使って対応する必要がありそうです: import { useEffect, useInsertionEffect, useRef } from 'react' ; import { useLocation } from 'react-router-dom' ; export function useOnLocationChanged ( callback ) { const refCallback = useRef(); const location = useLocation(); useInsertionEffect(() => { refCallback. current = callback; } , [ callback ] ); useEffect(() => { if (refCallback. current ) { refCallback. current ( location ); } } , [ location ] ); } なぜこのように useRef などを使う必要があるのでしょうか?例えば、 useOnLocationChanged が以下のように実装されていたとします: import { useEffect } from 'react' ; import { useLocation } from 'react-router-dom' ; export function useOnLocationChanged ( callback ) { const location = useLocation(); useEffect(() => { callback( location ); } , [ callback, location ] ); } この実装の問題点は useEffect の dependencies として指定されている callback 関数の参照が変わるたびに、 Location が変更されていないにも関わらず意図せず callback が呼ばれてしまう点です: function SomeComponent ( props ) { useOnLocationChanged(() => { // SomeComponentが再レンダリングされるたびに、意図せずこのcallbackが呼ばれてしまいます // ... } ); // ... } この問題を防ぐためには、現状では useRef を活用するなどの工夫が必要です。 useEffectEvent を使う場合 先ほどの処理は useEffectEvent を使うと簡略化することができます。 import { experimental_useEffectEvent as useEffectEvent } from 'react' ; function useOnLocationChanged ( callback ) { const location = useLocation(); const onLocationChanged = useEffectEvent(callback); useEffect(() => { onLocationChanged( location ); } , [ location ] ); } useEffectEvent は引数として関数を受け取り、関数を返却します ( onLocationChanged ) この useEffectEvent から返却された関数には以下のようなルールがあります: Effect ( useEffect )の外で呼んではいけません (例: レンダリングフェーズなどにおいて呼ぶことはできません) useEffect の dependencies からは省略する必要があります 他のコンポーネントのpropsなどに指定することはできません useEffectEvent を使うことで意図せず何度も callback が実行されてしまうことを防止できます。上記のコードの場合、 callback はReact Routerの Location オブジェクトが変更されたタイミングでのみ呼ばれます。 先ほどの useRef などを使った方法と比較して、 useEffectEvent を使うことによって、より直感的にやりたいことが実現できます。 useEffectEvent とは 先ほど使い方を紹介した useEffectEvent について、公式ドキュメントを参考により詳しく見ていきます。 react.dev github.com React の公式ドキュメントでは以下の3つを reactiveな値 と定義しています: Props State コンポーネント関数直下で定義された変数 そして、Reactには副作用を取り扱う方法として以下の手段があります: Effect - reactiveな値の変更時に実行されるreactiveな処理 イベントハンドラー - ユーザーの操作によって実行される非reactiveな処理 useEffectEvent はこれら2つの中間に相当するもので、Effect内で非reactiveな処理を実行したい場合に使用することが想定されます。 WebSocket を使ってリアルタイムにメッセージのやり取りを行う際のケースを例に見ていきます。 function ChannelView ( { channelID } ) { const logger = useLogger(); const dispatch = useDispatch(); const handleMessage = useCallback(( message ) => { logger. info ( `received: ${ message } ` ); dispatch(appendMessage(channelID, message)); } , [ channelID, dispatch, logger ] ); useEffect(() => { const ws = new WebSocket ( `/channels/ ${ channelID } ` ); ws. addEventListener ( 'message' , ( e ) => handleMessage(e.data)); return () => ws. close (); } , [ handleMessage, channelID ] ); // 省略... } props.channelID の変更時に新しい WebSocket 接続を張る処理は、ユーザーの操作ではなく reactiveな値の変更に基づいて実行する処理であるためEffectで処理しています。 この実装には一つ問題があります。 handleMessage は useEffect の dependencies に指定されているため、この handleMessage 変数が参照する関数が変更されるたびにEffectが再実行され、 WebSocket のコネクションが一から貼り直されてしまいます。これは意図せぬ挙動です。 handleMessage 内の処理は、Effectが依存しているreactiveな値 ( props.channelID )が変更されたタイミングで実行されるものではなく、 WebSocket から message を受信したタイミングで実行される処理です(非reactiveな処理) useEffectEvent によってこういった非reactiveな処理をEffectから抽出することができ、意図せぬタイミングで何度もEffectが実行されてしまう事態を防止できます。 function ChannelView ( { url } ) { const logger = useLogger(); const dispatch = useDispatch(); const handleMessage = useEffectEvent(( message ) => { logger. info ( `received: ${ message } ` ); dispatch(appendMessage(channelID, message)); } ); useEffect(() => { const ws = new WebSocket ( `/channels/ ${ channelID } ` ); ws. addEventListener ( 'message' , ( e ) => handleMessage(e.data)); return () => ws. close (); } , [ url ] ); // ... } 補足ですが、RFCによると useEffectEvent から返却される関数は on または handle から始まる名前の変数に格納されることが想定されているようです (元々、RFCにおいては useEvent という名前で提案されていたようですが、後ほど現在の useEffectEvent という名前へリネームされたようです) github.com github.com 現状ではどうしたらいいか? useEffectEvent は便利ではありますが、現在はまだ実験的APIのため、Reactの安定バージョンにおいては利用することができません。 調べてみたところ、いくつかのOSSにおいて自前でpolyfillを実装しているようです: Superset useEffectEvent が安定化された際の移行を容易にするために、 use-event-callback パッケージをベースに useEffectEvent を実装しているようです ( apache/superset#23871 ) Bluesky useInsertionEffect + useRef + useCallback をベースに useNonReactiveCallback というカスタムフックを実装しているようです ( src/lib/hooks/useNonReactiveCallback.ts ) Radix UIのWebサイト Blueskyとほぼ同様ですが、こちらはレンダリングフェースで実行された際に例外が発生する対応が行われています ( utils/use-effect-event.ts ) これらを参照することで、 useEffectEvent を使うべき場面の参考にもなりそうです。 ただ、紹介をしておいてアレですが、現状ではpolyfillなどは用意せずに、 useRef を使った解決策で対処しておくのが無難ではないかと個人的には思っています。 便利なAPIではあるものの、現状ではまだ useEffectEvent は実験的APIであり、正式に導入されるかどうかはわかりません。今後、引数や戻り値の形式などが変更される可能性も考えられます。 また、もし useEffectEvent が正式に導入された場合、公式でマイグレーションガイドの公開や eslint-plugin-react-compiler や eslint-plugin-react-hooks から移行のためのルールの提供なども考えられるため、それらの提供を待った方がスムーズに移行できる可能性も考えられそうです。現状では、まだ useRef を使った解決策で様子をみておいたほうが安全ではないかと考えています。 おわりに 以上、 useEffectEvent の紹介でした。とても便利なAPIなので、今後のバージョンで導入されることを楽しみにしています!
はじめに Atlantis とは 背景 ディレクトリ構成 モジュールの運用 AWS IAM role の設定 おすすめの Atlantis の機能 特定の条件がパスされないと atlantis apply を実行できないようにしたい atlantis apply が実行されていない Pull Request がマージされないようにしたい まとめ はじめに platform チームの渡部です。 RevComm の platform チームでは、チームトポロジーのプラットフォームチームのように、ストリームアラインドチームの開発における負担を軽減するために、サービスやプラットフォームの提供を行っています。 今回は、その取り組みの1つである Atlantis を使用した Terraform リポジトリの提供についてご紹介します。組織の拡大とともに Terraform の管理に課題を感じている方や Atlantis の導入を検討している方の判断材料になれば幸いです。 Atlantis とは Atlantis  は Terraform のデプロイを行う Issue Ops ツールの OSS です。 GitHub の Pull Request の issue comment にコマンドを入力して、 terraform plan や terraform apply を実行し、Pull Request 上でリソースのリリースまで行います。 我々のチームでは、Terraform リポジトリへの permission を持つ Github App が、ECS Fargate にホストされた Atlantis アプリケーションと通信して terraform コマンドを実行します。 この記事では、v0.30.0 の Atlantis を使用しています。 背景 我々が管理する Terraform リポジトリは、Atlantis を導入する以前から、複数のストリームアラインドチームが開発を行うモノリシックなリポジトリでした。 それ故に、Terraform Configurations が肥大化したり、リリースが滞留したりするなどの問題を抱えるようになりました。 そのような課題を解決するために、platform チームは Atlantis を導入しました。 これによって、CI/CD ツールのコードが複雑になることなく、以下のメリットを享受できると想定しています。 変更と関係のないリソースが壊れるリスクを背負う必要がなくなる 権限や責任を分離したい粒度や並行に開発を行える粒度で柔軟に HCP Terraform workspace を作成することができる 以降、モノリシックな Terraform リポジトリに Atlantis を導入するにあたり、工夫した点を紹介します。 ディレクトリ構成 Atlantis 導入後も、我々が管理する Terraform リポジトリは、複数のストリームアラインドチームが開発を行うモノリシックなリポジトリです。 そのため、ストリームアラインドチームごとにディレクトリを用意して、各チームがその配下に自由にディレクトリを作成することができるディレクトリ構成にしました。 各チームは、権限や責任を分離したい粒度や並行に開発を行える粒度に Terraform configurations を分割して(以下、コンポーネントと表現)、コンポーネントごとにディレクトリを作成します。 コンポーネントディレクトリ配下には、そのコンポーネントのモジュールや、そのコンポーネントのリソースがリリースされる AWS アカウントごとにディレクトリが分かれて管理されています。 下記はディレクトリ構成のイメージです。 . ├── stream_aligned_team_1 │ ├── component_1 │ │ ├── aws_account_1 │ │ │ ├── main.tf │ │ │ ├── providers.tf │ │ │ [omitted] │ │ │ │ │ ├── aws_account_2 │ │ │ ├── main.tf │ │ │ [omitted] │ │ │ │ │ └── modules │ │ ├── main.tf │ │ ├── variables.tf │ │ [omitted] │ │ │ └── component_2 │ ├── aws_account_1 │ [omitted] │ ├── stream_aligned_team_2 │ │ │ [omitted] │ [omited] このようなディレクトリ構成にすることで、コンポーネントのデプロイ時に関係のないリソースが壊れるリスクを回避することができます。 また、後述のモジュールのバージョニングの際に、このディレクトリ構成をtag の命名規則に反映しています。 モジュールの運用 Terraform module は source に Github リポジトリを指定して使用する ことができます。 我々が管理する Terraform リポジトリでは、この機能を使用して、自リポジトリを参照することで、バージョン管理した module を使用しています。 具体的には、下記のフローでモジュールのバージョニングとその使用を行っています。 ./stream_aligned_team_1/component_1/modules  配下に  moduleA  を作成して main ブランチにマージします。 Github の Releases 機能を使用して  stream_aligned_team_1-component_1-v1.0.0  という tag を作成してリリースします。 ./stream_aligned_team_1/component_1/aws_account_1/main.tf では、以下のように  moduleA  を指定して使用します。 module "this" { source = "git::ssh://git@github.com/{org_name}/{repo_name}//stream_aligned_team_1/component_1/modules/moduleA?ref=stream_aligned_team_1-component_1-v1.0.0" } このとき、tag 名は Terraform リポジトリ内で一意になるように {stream_aligned_team_name}-{component_name}-{semantic_versioning}  の命名規則に沿うように行います。 これにより、チームごとにモジュールのバージョニングが可能になります。 また、開発環境用の AWS アカウントでは、module を Github リポジトリ参照ではなく、相対パスによる参照を行なっています。 これにより、わざわざ main ブランチへのマージと Release を行うことなく、module の動作確認やデバッグを行いながら開発を行うことができます。 AWS IAM role の設定 Atlantis は 1 つの AWS アカウントにのみホストされています。 したがって、各環境へ  terraform apply  するためには、各環境の terraform コマンドを実行するための IAM Role を assume role しなければなりません。構成は下記のようになります。 また、我々が管理する Terraform リポジトリでは、ファイルレイアウトによってステートファイルを分離する方針をとっており、各 AWS アカウントのリソースの tfstate は、その AWS アカウントの remote backend(S3 bucket と DynamoDB table) に配置されています。 各 AWS アカウントの remote backend にアクセスするときにも、下記のように IAM role を assume role して tfstate を更新しています。 これにより、各環境は完全に分離されます。 backend "s3" { region = "ap-northeast-1" bucket = "account1-tfstate" key = "stream_aligned_team_1/component_1/terraform.tfstate" dynamodb_table = "account1-locks" encrypt = true assume_role = { role_arn = "arn:aws:iam::account1:role/account1-tf-exec-role" external_id = "account1-external-id" } } おすすめの Atlantis の機能 Atlantis の機能は多岐に渡ります。 基本的な機能としては、 atlantis plan の実行前に該当のディレクトリをロックする Looking 機能や、Pull Request 作成時に atlantis plan を自動的に実行する Autoplanning 機能などがあります。 最後に、以下のようなことを実現したいときに、Atlantis のどの機能を使えばよいかを紹介して終わりにしたいと思います。 特定の条件がパスされないと atlantis apply を実行できないようにしたい Atlantis には Command Requirements 機能という機能があります。 これは、 atlantis plan や atlantis apply コマンドを実行する前に、特定の条件を満たすことを強制する機能です。 例えば、Atlantis の設定ファイルに下記のように設定すると、Pull Request が「マージ可能( Mergeable )」な状態でなければ atlantis apply を実行できなくなります。 repos : - id : /.*/ apply_requirements : [ mergeable ] 「マージ可能」な状態は、Atlantis を使用する VCS によって異なります。 Github の場合は、 branch protection rule をパスした状態を指します。 あとは、Github の Settings で有効にしたい条件をチェックして、マージ可能な状態でなければ、下記のように atlantis apply は失敗します。 atlantis apply が実行されていない Pull Request がマージされないようにしたい Atlantis を使用していると、 atlantis apply を実行する前に、誤って Pull Request をマージしてしまうことがあります。 こうなると、いちいち Pull Request を作り直すことになり、面倒です。 なので、下記のように Github の status checks に atlantis/apply を追加したくなります。 追加して、いざ atlantis apply を実行してみると、上記の Mergeable で試したときと同じように失敗します。 これは、 Mergeable を有効にすると、Atlantis が Github の status checks もパスしたかを確認してしまうためです。 つまり、 atlantis apply を実行するために atlantis/apply をパスしなければならないという八方塞がりの状態になってしまいます。 そんなときのために、Altantis には atlantis apply を実行する前に、 atlantis/apply の status check をスキップして、マージ可能かどうかをチェックすることができるオプションが用意されています。 それが --gh-allow-mergeable-bypass-apply です(環境変数でも設定可能です)。 atlantis server --gh-allow-mergeable-bypass-apply これで、Mergeable を設定した状態で、 atlantis apply が実行されていない Pull Request がマージされないようにすることができます。 まとめ Terraform のデプロイを行うツールである Atlantis  と、Atlantis を導入した Terraform リポジトリの運用について紹介しました。 Atlantis には、今回紹介した機能以外にもたくさんの機能があります。 ngrok を使用すればローカル環境でも気軽に試してみることができますので、興味のある方はぜひ動かしてみてください。
背景 弊社ではさまざまなログを DataDog に集約しているのですが、一部サービスで EKS on Fargate を利用しており、datadog-agent + fluent-bit のサイドカー構成で DataDog にログを送っています。 その中でも Job を使用した場合にうまく DataDog にログが送れず、困っていました。 Job だとサイドカーが動いてくれない + Job終了時にサイドカーが終了してくれない という状況で、Jobに関してはDataDogを諦めて kubectl や ArgoCD 経由でログを一生懸命読む、という運用が続いていました。 結論 k8s 1.29 からデフォルトで有効となったサイドカーコンテナの機能を使用することで問題は解決しました。正解は公式ドキュメントにバッチリ書いてあり、今まで我々は何をそんなに困っていたんだろう?ということをつらつらと言い訳していこうと思います。 kubernetes.io 詳細 サイドカーが機能しない件 DataDog にログが出ず...これは、起動順序に問題があるようでした。メインのコンテナよりもサイドカーを先に起動させる必要があるところまでわかり、manifest の containers で記載順序を変えて対応しようとしました。実際このとき DataDog にログは出たのですが、サイドカーが終了しない問題の解決が難しくなりました。 サイドカーが停止しない件 Job の場合、仕事を完了したメインコンテナは仕事中のサイドカーを残して一人で終了してしまいます。これだと、終わった Job がいつまでも生き残ってしまいます。 そこでメインが終了する際にサイドカーを終了したり、逆にサイドカーがメインコンテナを監視してメイン終了を検知したら自分も終了する command を書いたりの工夫をしてみたのですが、サイドカーが機能しない件の対応で containers の記載順序を変更したところうまくいかなくなりました。 サイドカーコンテナ機能の使用 行き詰ったので心を落ち着けて公式ドキュメントをじっくり読むことにしたところ、わりとすぐに答えが見つかりました。結論にも記載した通り、k8s 1.29 からはデフォルトでサイドカーコンテナの機能が使用できるようになっていて、containers ではなく initContainers にサイドカーを指定するだけでOKでした。 サイドカーコンテナは initContainrs として指定するため、起動順序はメインコンテナよりも先となります。また、サイドカーの restartPolicy は Always を指定します。こうするとメインコンテナが終了した場合のみサイドカーが停止するようになります。 なぜ自前で四苦八苦していたのか 事前に軽く調べた際に「Job には自動でサイドカーを終わらせる方法がなく、自前で用意する必要がある」というブログを参照しており、ちょっと古い記事ではあったのですが、他に情報もなく、鵜呑みにしていたんですね。 公式ドキュメントを読み込んでおくのは基本のようにも思えますが、現場でスピード感を持って判断していく上で、ライトに読めてしまうブログやAIの回答を参考にして済ませてしまうことは実際よくあると思います。 そんな忙しい我々のためにも、軽く調べたら出てくるように情報発信をしていくことが大切だなと感じました。さらに言えば、不思議な力でこの記事が数ヶ月前の我々に届いてくれたらとても嬉しい。そういったサービスには対応していませんか?サンタさん、何卒宜しくお願い致します。 以上
はじめに MiiTel Analytics Platformチームの小門です。 RevCommではサービス基盤にAWSとして利用していますが、IaCには主にTerraformを用いています。 基本的にTerraformコードはGitHubで管理され、プルリクエストを介してCI/CDを自動実行してリソースの構築、構成変更を行います。 最近、Terraformコードを管理するリポジトリを新設したり既存のコードをリファクタリングする機会があったためナレッジを共有します。 この記事では、Terraform v1.5以降に導入された機能である import / removed / moved ブロックを活用する方法を紹介します。 動機 IaCの活用度合いはサービスや会社、チームの状況に大きく左右されると思います。 必ずしもプロジェクトの初期からIaCが整備されるとは限らないし、サービスや組織の変化に応じてコード管理の都合も変わることでしょう(弊社が正にそうです)。 Terraformは機能が豊富なため上記のような事情に柔軟にアプローチできます。 例えば既存のリソースをIaC管理に取り込む場合は terraform import 、逆に特定のリソースをIaC管理から除外する場合は terraform state rm などのコマンドがあります。 しかしIaCという特性上、手動コマンド実行によるtfstateを操作するのはチーム開発において不都合があります。 例えばチームの誰かによって同じタイミングでCI/CDが起動されると、お互いの変更が競合したりどちらかの変更が後勝ちするような可能性が考えられます。 このような場合でもTerraformの機能を有効活用することでチーム開発に支障をきたさずリファクタリングすることができます。 サンプルコード AWSリソースを管理するための最小のサンプルとして、ECSクラスターを1つ管理するケースを考えてみます。 初めのディレクトリ構成: . └── provider.tf provider.tf terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" } 検証コードのバージョン Terraform v.1.9.8 AWS provider: v5.78.0 import ブロック import ブロックはTerraform v1.5以降で利用可能です。 terraform cliの terraform import [ADDR] [ID] と等価です。 import { to = [ ADDR ] id = "[ID]" } IaC管理されていないAWSリソースであるECSクラスター revcomm-2024-adventcalendar を新たにIaC管理に含めます。 ※terraform cliだと terraform import aws_ecs_cluster.foo revcomm-2024-adventcalendar # main.tf resource "aws_ecs_cluster" "foo" { name = "revcomm-2024-adventcalendar" } # import_ecs_cluster.tf import { to = aws_ecs_cluster.foo id = "revcomm-2024-adventcalendar" } . ├── import_ecs_cluster.tf ├── main.tf └── provider.tf ※好みですが、 import ブロックはいずれ削除可能なため main.tf ではなく別ファイルにすることで後で丸ごと消せるようにしています。 この状態で terraform plan を実行すると以下のようになります。 $ terraform plan ... Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. 1 to import かつ 0 to add, 0 to change のため新たにリソースは作成されず、また変更もされないことが分かります。 import ブロックはコマンドの手動実行ではなく コード変更によって IaCの管理対象を操作することができます。 ※以降のコードは terraform apply を実行 && import_ecs_cluster.tf を削除した状態とします。 removed ブロック removed ブロックはTerraform v1.7以降で利用可能です。 その名の通りリソースをIaC管理から外す(リソース実体は削除しない)ためのもので、 import ブロックとは逆の用途です。 terraform cliの terraform state rm と等価です。 terraform state rm [ADDR] removed { from = [ ADDR ] lifecycle { destroy = false } } lifecycleブロックで destroy = false としてリソースを削除しないようにできます。 # main.tf removed { from = aws_ecs_cluster.foo lifecycle { destroy = false } } # resource "aws_ecs_cluster" "foo" { # name = "revcomm-2024-adventcalendar" # } また(IaC管理から)削除するリソースのTerraformコードも合わせて削除する必要があります。 removed ブロックも後から削除可能なため、私のチームでは初めコメントアウトに留め、その後 removed ブロック自体の削除時にresourceブロックも削除する形で運用しています。 planの実行結果は以下のようになります。 $ terraform plan # aws_ecs_cluster.foo will no longer be managed by Terraform, but will not be destroyed # (destroy = false is set in the configuration) ... Plan: 0 to add, 0 to change, 0 to destroy. 0 to add, 0 to change, 0 to destroy のため、実際のリソースに変更は起きません。 movedブロック removed ブロックはTerraform v1.7以降で利用可能です。 Terraformコード上の管理名(ADDR)をリネームする場合に使用します。 terraform cliの terraform state mv と等価です。 terraform state rm [SOURCE] [DESTINATION] moved { from = [ SOURCE ] to = [ DESTINATION ] } 上記までの例で、ECSクラスター revcomm-2024-adventcalendar のTerraformコード上の管理名を(敢えて) foo としていました。 しかし、この命名では役割が分かりづらいためいずれ問題が起きることでしょう。 これを foo から api にリネームするリファクタリングを 安全に 行うことができます。 + moved { + from = aws_ecs_cluster.foo + to = aws_ecs_cluster.api + } - resource "aws_ecs_cluster" "foo" { + resource "aws_ecs_cluster" "api" { name = "revcomm-2024-adventcalendar" } planの実行結果は以下のようになります。 $ terraform plan # aws_ecs_cluster.foo has moved to aws_ecs_cluster.api ... Plan: 0 to add, 0 to change, 0 to destroy. aws_ecs_cluster.foo を aws_ecs_cluster.api に変更しつつ、実際のリソースに影響がないため安全にリファクタリングを実行できます。 まとめ Terraformのリファクタリングを チーム開発の中でも安全に 実施する方法と簡単なケーススタディを紹介しました。 特に import ブロックと removed ブロックを併用することでリポジトリを跨いだリファクタリング、IaCコードの分割などが行えるのがとても気に入っています。
By Kenji Yamauchi (Analytics Team) In this blog post, we introduce our new Blue/Green-based upgrade strategy for our Amazon EKS powered RevComm analytics platform along with an automation to streamline the process. This is the English version of a similar blog. Please check this post for the Japanese version. RevComm’s Analytics Platform Before we get into the details of the upgrade, a brief introduction to RevComm's analytics infrastructure is in order: although RevComm offers several products, such as MiiTel and MiiTel Meetings, they all share a single infrastructure for performing analytics, such as transcription. This analysis infrastructure does not run from a single application but consists of multiple applications divided into modules such as speech recognition and other analytics functions, which are currently hosted within a single EKS cluster. AWS resources including the EKS cluster and middleware such as the AWS Load Balancer Controller are managed using Terraform. In addition, Kubernetes manifests for applications running in the cluster are managed in a separate repository, and deployed by referencing the main branch with Argo CD running in the cluster (Pull-type GitOps). Upgrade strategy for EKS cluster Since our adoption of EKS, the RevComm analysis infrastructure has been using the in-place method, i.e. upgrading the version of the existing cluster directly. As mentioned above, RevComm's analysis infrastructure uses IaC, so it can be done easily with only a few configuration changes, but there are some drawbacks. More concretely, the in-place strategy conducts rolling updates for the resources including the node groups as described in the EKS Best Practices Guides . As a result, we faced potential downtime during cluster upgrades and couldn't rollback if issues arose. Moreover, the upgrade process took several hours to complete. On the other hand, when going for a Blue/Green strategy upgrade, we prepare a new version cluster alongside the current cluster and let the traffic into the new cluster gradually. As we discard the older cluster only after confirming the behavior of the new cluster, we avoid the aforementioned problems. However, there’s overhead since we have to prepare a new cluster. Nevertheless, we opted for the Blue/Green strategy to improve the availability of the system. Implementation How to switch cluster We implemented the Blue/Green strategy following the article Blue Green Migration of Amazon EKS Blueprints for Terraform because our architecture was similar. Assuming that the version of a current (blue) cluster is 1.25 and one of a new cluster (green) is 1.28, the flow of switching clusters is as follows: Create a new cluster and install middleware through Terraform Deploy the applications on the new cluster with Argo CD Change the manifests Use external-dns to distribute traffic via Ingress annotations with Route 53’s weighted routing. Specify values of set-identifier and aws-weight Store new changes as a feature branch of the manifests repository and refer to them from the Argo CD of the new cluster. The Argo CD of the current cluster still refers to the main branch. Delete the current cluster after confirming the behavior of the new cluster Merge the feature branch into the main branch and make the Argo CD of the new cluster to refer to the main branch. Automation Although we can complete those steps as mentioned before by adjusting the variables of the Terraform scripts and modifying the parts of the manifests, the steps are relatively complicated compared to the in-place strategy. Therefore, we automated them with GitHub Actions to prevent dependence on individual members and operational errors. We automated the two processes: the creation of a new cluster and the deletion of the current cluster. Please look at the diagram below. The Terraform code for the analysis infrastructure has directories for managing backends and variables for each environment (development, staging, and production) under the vars directory, hereafter simply referred to as environment settings. Each environment setting is managed with an identifier for each environment, such as cluster-YYYYYMM . In addition, the environment settings for the current cluster are pointed to by symbolic links. When we create a new cluster with this structure, we need to generate new environment settings, create a new cluster by terraform apply , and recreate the symbolic link. On the other hand, when we remove the current cluster, we need to remove the middleware and the applications hosted in the current cluster, the current cluster and the environment settings for the current cluster. To prevent operational errors that could occur during manual processes, we automated these steps using GitHub Actions. The Actions take identifiers and cluster versions as inputs and perform the entire process—creating, switching, and deleting clusters, as well as deploying applications to the new cluster—in a matter of minutes. Conclusion In this article, we introduced our migration from an in-place EKS upgrading strategy to Blue/Green. Compared to the in-place method, we needed to automate and establish procedures to avoid increasing man-hours. Still, as we had the groundwork of IaC, we were able to introduce the Blue/Green method with maximum benefit. The analysis infrastructure had few stateful elements, and there were few things to consider on the application side, which also made it compatible. Thank you for reading and have a happy upgrading time!
2024年10月25日(金)~27日(日)にインドネシアで開催された PyCon APAC 2024 にバックエンドエンジニアの 松土 慎太郎、陶山 嶺、小門 照太の3名が登壇しました。 tech.revcomm.co.jp 今回はイベントの振り返りとして登壇資料と登壇者の感想を紹介します。 登壇振り返り Empowering your real life with Raspberry Pi 概要: 音声認識及び音声合成を活用して、その日のスケジュールを教えてくれる音声ボットをRaspberry Piによって作ります。初学者の方を対象に、ハンズオン形式でRaspberry Piのパワーを日常生活に取り入れる案内をいたします。 登壇者: 松土 慎太郎 発表資料: docs.google.com 登壇者の感想 PyCon APAC 2024で「Empowering Your Real Life with Raspberry Pi」というテーマで発表しました。英語は得意ではありませんが、ハードウェア開発に興味を持ってもらえるよう、構成の工夫や発表の練習にいつも以上に力を入れました。その結果、参加者から多くの質問や反応をいただき、関心を引くことができたと感じています。 また、弊社RevCommのジャカルタオフィスを訪問し、現地のスタッフと交流する機会も作ることができました。直接顔を合わせて話すことで、日頃の業務のつながりをより深めることができたのは大きな収穫です。 このような貴重な経験を与えてくれた会社やチームに感謝しています。今回の学びや気づきを、今後の業務に還元していきたいと思います。 The power of Python's type hints: Case studies focusing on famous libraries 概要: 近年のPythonは型ヒントの強化が活発で、メジャーアップデートのたびに便利な機能が追加されています。 さらに型ヒントを活用するライブラリやツールも多く登場し、コミュニティからの絶大な人気を集めています。 (中略) Pythonの型ヒントはまだまだ多くの可能性を秘めていると思います。 本セッションを通じて、普段の開発で型ヒントをより便利に活用し、新たなアイディアを自身の手で具体化していきましょう。 登壇者: 陶山 嶺 発表資料: docs.google.com 登壇者の感想 PyCon APAC 2024では「The power of Python's type hints: Case studies with a focus on well-known libraries」というタイトルで、FastAPI、Pydantic、SQLAlchemyといったRevCommの業務でもわたしがよく利用しているライブラリの内部の実装を紹介するトークを行いました。 今回はわたしにとって初めての海外イベント参加、初めての英語で行うトークだったので、当然ながら準備は大変でした。しかし、初めてだからこそしっかりと準備を行なっていたので、当日が近づくにつれて徐々に「大丈夫」という気持ちに変化していました。いまは無事にトークも終わり、達成感を感じています。 国内のイベントではこれまで何度かトークをしていましたが、今回のPyCon APAC 2024では初めてプロポーザルを出したときのような新鮮な気持ち、チャレンジ精神で向き合うことでき、得られたものもその分大きかったです。また、今回はジャカルタオフィスのメンバーたちとも交流でき、現地で直接コミュニケーションができたからこそ学べたことも多かったです。機会があればまた挑戦したいと思います。 Developing Python Libraries Using Rust 概要: Python以外の言語で実装された機能(モジュール、クラス、関数)をPythonのライブラリとして使用することが可能です。 有名なものでは Numpy / Pandas は高速化のために主にC言語をベースに実装されています。 最近ではC/C++以外にもRust言語の活用が注目されています。 本セッションでは、Rust を利用してPythonライブラリを開発する利点や手順などを解説します。 また実際にRustが使用されているライブラリの実例を紹介します。 登壇者: 小門 照太 発表資料: docs.google.com 登壇者の感想 私のトークタイトルは「Developing Python Libraries Using Rust」でした。 ※2024年9月PyCon JPのトーク「Rustを活用したPythonライブラリの開発」を英語に翻訳したものです。 今回このタイトルを選んだ理由は私自身がRust言語を学びたかったためであり「登壇ドリブン学習」でした。 PythonのカンファレンスでRustを中心にしたトークは少し異質だったかもしれませんが、Python開発者が持ち帰ることのできる有益な内容を心掛けました。 結果として、会場では多くの方が聞きに来てくれたり発表後に質問や感想を言いに来てくれて充実した登壇となったと思います。 英語での登壇は初めてでしたが、今回は同僚2名(+ つい最近退職した元同僚が1名)と同行できたことはとても心強かったです。 プロポーザルを提出するきっかけを含め、今回のような活動ができる仲間がいるのはとても幸せなことだと感じています。 おわりに 以上、PyCon APAC 2024 への登壇に関するまとめになります。技術評論社様のWebページにおいても弊社の小門 照太が寄稿したPyCon APAC 2024への参加レポートが公開されているため、よろしければこちらもご参照いただければと思います! gihyo.jp RevCommでは今回の PyCon APAC 2024 の開催地であるインドネシアにおいてもサービス展開しており、ジャカルタにオフィスがあります。今回の PyCon APAC 2024 への登壇に伴い、現地のジャカルタオフィスに所属するスタッフと交流する機会を作ることができるという点も大きなモチベーションとなり、今回、3名のメンバーが登壇する運びとなりました。 後日、弊社のnoteにおいても PyCon APAC 2024 の登壇に関するインタビュー記事を掲載予定です。もしご興味がありましたらぜひそちらもご覧いただければと思います! note.com
MiiTel Platform チームの小門です。 2024年10月25日(金)~27日(日)に開催される PyCon APAC 2024 に RevComm のエンジニア陶山 嶺と松土 慎太郎、わたし小門 照太の3名が登壇します イベント概要 https://2024-apac.pycon.id/ 日程: 2024年10月25日(金)~27日(日) 会場 Universitas Nahdlatul Ulama Yogyakarta チケット ※オンラインチケットもあります 登壇情報 発表一覧とスケジュールは以下に公開されています https://pretalx.com/pycon-apac-2024/talk/ Empowering your real life with Raspberry Pi Harness the Raspberry Pi to create a voice-activated bot that tells you your schedule for the day, using both speech recognition and synthesis. Designed for beginners, this session will guide you through a hands-on project that brings the power of Raspberry Pi to your everyday life. 日時: 2024年10月26日 11:30-12:00 (Asia/Jakarta) 登壇者: Shintaro Matsudo リンク: https://pretalx.com/pycon-apac-2024/talk/HE8QDG/ The power of Python's type hints: Case studies focusing on famous libraries This session will show how these libraries implement the ideas. I believe that type hints in Python still has a lot of potential. Through this session, you will be able to use type hints more conveniently and flesh out new ideas by yourself. 日時: 2024年10月26日 13:00-13:30 (Asia/Jakarta) 登壇者: Rei Suyama リンク: https://pretalx.com/pycon-apac-2024/talk/RH7GPM/ Developing Python Libraries Using Rust In this talk, I will explain the advantages and procedures for developing Python libraries using Rust. I will also introduce examples of libraries where Rust is being used. 日時: 2024年10月26日 15:45-16:15 (Asia/Jakarta) 登壇者: Shota Kokado リンク: https://pretalx.com/pycon-apac-2024/talk/V8V7EW/ おわりに 私と陶山の2名は先月開催されたPyCon JP 2024に引き続きの登壇( リンク )、 また3名とも昨年日本で開催されたPyCon APAC 2023に引き続きPyCon APACでの登壇となります。 もし現地(ジョグジャカルタ)で参加される方がいましたらお話できると嬉しいです。 英語での登壇が初めてのためチャレンジとなりますが、楽しんで発表してこようと思います!
はじめに Rules of React という安全で効果的なReactアプリケーションを記述するためのルールがReact公式から公開されました。 Writing idiomatic React code can help you write well organized, safe, and composable applications. These properties make your app more resilient to changes and makes it easier to work with other developers, libraries, and tools. These rules are known as the  Rules of React . https://github.com/reactjs/react.dev/blob/40d73490733a1665596eee8b547278231db3b8e3/src/content/reference/rules/index.md より引用 Rules of Reactに反することにより、バグの原因となってしまったり、理解することが難しいコードになる恐れがあると言及されています。 They are rules – and not just guidelines – in the sense that if they are broken, your app likely has bugs. Your code also becomes unidiomatic and harder to understand and reason about. https://github.com/reactjs/react.dev/blob/40d73490733a1665596eee8b547278231db3b8e3/src/content/reference/rules/index.md より引用 調べてみて、このRules of Reactは「そもそもReactとは一体どのようなフレームワークなのか?」を理解する上でも有用なのではないかと思いました。 そこで、この記事ではRules of Reactを題材に、あらためてReactとはどのようなフレームワークなのかについて見ていきたいと思います。 想定読者 この記事はどちらかというとReactの初学者〜ある程度慣れてきた方を想定して記述しています。 抽象的な内容が結構多いので、もし具体的な内容にのみ興味があるという場合は、終盤の「 実践編 」の内容だけ参照ください。 注意 今後、リリースされる予定のReact v19では use() などの新しいAPIが導入される予定です。この記事はそれらのAPIについては考慮に入れず執筆していますが、今後、Reactにおける副作用の取り扱い方法にも変化が起きる可能性も考えられます。しかし、Reactの背景にある考えについてはそう大きくは変わらないと思います。 Components and Hooks must be pure (コンポーネントとフックは純粋でなければならない) Rules of Reactに関するドキュメントの一つに Components and Hooks must be pure (コンポーネントとフックは純粋であるべき)というページがあります github.com 直訳すると「コンポーネントとフックは純粋でなければならない」というような感じではないかと思います。では「純粋」とは一体何を指しているのでしょうか? これはRules of Reactを理解する上で重要な考えではないかと思うため、まずは純粋関数というものについて見ていきます。 純粋関数と参照透過性について 以下のような関数があったとします。 const plus = ( a , b ) => a + b ; const double = ( x ) => 2 * x ; const inc = ( x ) => 1 + x ; これらの関数は、 引数のみに依存し、副作用が存在しない という共通した特徴を持ちます。こういった関数を 純粋関数 と呼びます。 まず、純粋関数の特徴である「 引数のみに依存する 」という点についてはわかりやすいのではないかと思います。上記の関数はすべて引数以外の要素には全く依存をしていません。 では、もう一方の「 副作用が存在しない 」とはどういうことでしょうか? 大雑把に説明すると、外部の状態を変更する処理は副作用があると説明できるかと思います。具体的には、以下のような処理は副作用であると思います。 グローバル変数やパッケージローカルな変数の上書き 乱数の生成 localStorage へのデータの保存 fetch() によるAPIの呼び出し Console への出力 純粋関数は引数以外の要素に依存しないため、ある純粋関数に 同じ引数の組み合わせが与えられた場合、常に同じ結果を返す という特徴があります (このような性質を参照透過性と呼びます) 次は逆に純粋ではない関数の例を見てみます。 const formatDate = () => { const date = new Date () ; const y = date . getFullYear () ; const m = String ( date . getMonth () + 1 ) . padStart ( 2 , '0' ) ; const d = String ( date . getDate ()) . padStart ( 2 , '0' ) ; return ` ${ y } / ${ m } / ${ d } ` ; } ; この formatDate 関数はなぜ純粋関数ではないのでしょうか?以下の行がポイントです。 const date = new Date () ; new Date() は呼び出しのたびに結果が変わる処理(=参照透過性がなく、純粋ではない)であり、この new Date() を呼んでいる formatDate も同様に純粋ではなくなります。 それでは、この formatDate を純粋関数にするためにはどうしたらいいでしょうか?この場合は、 date を formatDate 関数内で生成するのではなく、引数として受け取るようにすれば純粋関数にできます。 // フォーマット対象の日付を引数で受け取る const formatDate = ( date ) => { const y = date . getFullYear () ; const m = String ( date . getMonth () + 1 ) . padStart ( 2 , '0' ) ; const d = String ( date . getDate ()) . padStart ( 2 , '0' ) ; return ` ${ y } / ${ m } / ${ d } ` ; } ; この formatDate の例では new Date() を例に説明しましたが、以下のように関数の外の変数を変更するような関数も純粋関数ではありません。 // 関数の外の変数に依存している let queue = [] ; const enqueue = ( x ) => { queue . push ( x ) ; } ; const dequeue = () => queue . shift () ; この場合もキューを引数として受け取るようにすると、副作用を排除することができます。 const enqueue = ( queue , x ) => { return [ ...queue, x ] ; } ; const dequeue = ( queue ) => { const [ head , ... rest ] = queue; return [ head, rest ] ; } ; const [ item , queue ] = dequeue(enqueue(enqueue(enqueue( [] , 1 ), 2 ), 3 )); item; // => 1 queue; // => [2, 3] また、JavaScriptにおいてよく見かける、引数に与えられたオブジェクトを直接変更するような関数も純粋関数ではありません。 const completeTask = ( task ) => { task . status = 'completed' ; } ; このようなケースでは、以下のように新しいオブジェクトを生成して返却してあげれば、副作用を排除することができます。 const completeTask = ( task ) => ({ ... task , status : 'completed' }) ; それ以外にも、フロントエンドにおいてよく見かけるケースが多いと思われる localStorage や fetch などのAPIに依存した関数も純粋関数ではありません。 // localStorageという引数以外の外部のリソースに依存している const readItems = ( id ) => { const data = localStorage . getItem ( id ) ; return parseItems ( data ) ; } ; // fetchによってHTTPリクエストを送信している const getUserByID = ( id ) => { const res = await fetch ( buildUserByIDURL ( id )) ; return await res . json () ; } ; ただし、上記のように localStorage へのデータの永続化や復元、 fetch() によるHTTPリクエストの送信などはアプリケーションを構成する上で非常に重要な要素です。副作用そのものは便利なアプリケーションを開発するためには必要不可欠なものです。 副作用の存在をきちんと認識し、むやみな乱用を避けたり、きちんと分離することなどが重要ではないかと思います。 純粋関数のメリット 純粋関数について紹介しましたが、具体的にこの純粋関数を使うメリットとは何でしょうか?以下のような点が考えられるでしょう。 1. 結果が予測しやすい 引数以外の要素に依存せず外部要因によって計算結果が変わらないため、計算結果を予測しやすいケースが多いと思います。 また、純粋関数には外部依存がないため、大抵の場合、テストダブル(モックやスタブ、フェイクなど)を用意する必要がなくユニットテストを容易に記述することができます。 2. コードの再利用がしやすい 外部の状態やリソースなど、特定の外部コンテキストへの依存が少なく、引数以外の要素には依存しないため、コードを変更することなく様々な箇所で再利用がしやすいです。 3. 計算結果を安全にキャッシュできる 純粋関数には副作用がなく、同じ引数で呼ばれれば必ず同じ結果を返す性質があるので、関数の計算結果を安全にキャッシュすることができます。 具体例として、メモ化と呼ばれる関数の最適化手法があるため、紹介します。 ja.wikipedia.org 以下はメモ化の単純化した例です。 memoize は指定された関数 fn をメモ化します。 // 指定された関数fnをメモ化します function memoize ( fn ) { const cache = new Map (); return (... args ) => { const key = JSON . stringify (args); if (cache. has (key)) { return cache. get (key); } const result = fn(); cache. set (key, result); return result; } ; } この memoize 関数は、以下のように使用します。 const add = ( a , b ) => a + b; // addのメモ化バージョン const memoizedAdd = memoize(add); memoizedAdd( 1 , 2 ); // => 3 memoizedAdd( 1 , 2 ); // => 3 (引数の組み合わせが同じため、add(1, 2)を呼ばずにキャッシュされた結果が返却されます) memoizedAdd( 2 , 3 ); // => 5 (引数の組み合わせが異なるため、add(2, 3)が呼ばれます) 純粋関数は引数以外の要素には依存せず、副作用も存在しないため、安全にメモ化を行うことができます。 上記の例は説明のためだいぶ簡略化されています。もし実際にメモ化を使いたい場面が出てきた際は、以下のような本格的なライブラリを使うことをお勧めします。 lodashの memoize ramda.jsの memoizeWith メモ化について紹介しましたが、Reactには React.memo() や useMemo() などのAPIがあります。これらはコンポーネントなどに対してメモ化を適用するためのAPIであると考えられるかと思います。 Reactコンポーネントを純粋関数として実装する それでは、具体例の一つとしてこの純粋関数をReactのコンポーネントに当てはめて考えてみます。 Reactコンポーネントは、StateやEffectなどを持たなければ、 props を受け取り VNode を返す純粋関数として実装することができます。 github.com 以下のコンポーネントは props.users にのみ依存しており、 props.users が同じであれば、何度呼んでも同じレンダリング結果が得られます。 function UserList ( props ) { return ( < ul > { props . users . map (( x ) => ( < li key = { x . id } > { x . name } </ li > )) } </ ul > ) ; } 副作用を持つReactコンポーネントについて それでは逆にReactにおいて副作用を扱いたい場合、つまり純粋ではないコンポーネントを実装したい場合はどうすれば良いのでしょうか?こういった場合にEffectやイベントハンドラーなどを利用します。 function UserList () { const [ isLoading , setIsLoading ] = useState( false ); const [ users , setUsers ] = useState( [] ); // 副作用を起こすためにEffectを使う useEffect(() => { const ac = new AbortController (); setIsLoading( true ); fetch ( 'https://api.example.com/users' , { signal : ac.signal } ) . then (( res ) => res.json()) . then (( users ) => setUsers(users)) . finally (() => setIsLoading( false )); return () => ac. abort (); } , [] ); // ... 省略 ... } Reactと参照透過性の関係について Reactのコンポーネントの実態はprops, State, またはContextを受け取りVNodeを返す関数であり、入力として受け取ったprops, State, またはContextが同じであれば同じレンダリング結果が得られます。このようにReactではフレームワークレベルで参照透過性が意識されていることが伺えます。これによりUIのレンダリング結果を予測しやすくしたり、デバッグを行いやすくなることなどが期待されます。 Rules of Reactの各ルールについて github.com 前置きが長くなりましたが、上記までの純粋関数や参照透過性などに関する内容などを踏まえて、あらためてRules of Reactで紹介されているルールについていくつか見ていきます。 Side effects must run outside of render (副作用はレンダリングフェーズの外で実行しなければならない) これは副作用はレンダリングフェーズの外で行うべきであるというルールで、具体的にはイベントハンドラーやEffect ( useEffect )によって副作用は実行されるべきです。 例えば、コンポーネント内において、コンポーネントの外の変数を変更しているようなケースはこのルールに違反します。 let id = 0 ; function Message ( props ) { id = id + 1 ; // <= ここ return ( < div > Message # { id } : { props.message } </ div > ); } 上記コンポーネントにおいて問題なのは以下の箇所です。 let id = 0 ; function Message ( props ) { id = id + 1 ; // <= ここ // ... 省略 ... } ここではコンポーネントのレンダリングフェーズにおいて、コンポーネント外の変数が上書きされてしまっています。(副作用が発生している) このように、コンポーネントのレンダリングフェーズにおいて副作用を発生させることはReactにおいては行うべきではありません。もし副作用を発生させたい場合は、Effectやイベントハンドラーなどを利用する必要があります。 このようにレンダリングフェーズにおいて副作用が発生しないことを前提とすることで、Reactでは優先度に応じて特定のコンポーネントだけレンダリングを中断したり再開したりする余地が生まれます。 Components and Hooks must be idempotent (コンポーネントとフックはべき等でなければならない) 直訳すると コンポーネントとフックはべき等でなければならない といった意味になるかと思います。 べき等であるとはなんでしょうか? 大雑把に言えば、ある処理を何回行っても同じ結果が得られる性質を指します。 glossary.cncf.io 純粋関数は同じ引数が与えられれば常に同じ結果を返すため、引数が一致している場合、純粋関数にはべき等性が担保されます。 ではこのルールについて、先ほど使ったコンポーネントを再掲して紹介します。 let id = 0 ; function Message ( props ) { id = id + 1 ; // <= ここ return ( < div > Message # { id } : { props.message } </ div > ); } 上記のコンポーネントは、この Components and Hooks must be idempotent ルールに違反しています。どうしてかというと、このコンポーネントは id という外部の変数に依存しているので、例え同じ <Message message='foo' /> という呼び出しを行った場合でも、実行の度にレンダリング結果が変わってしまいます (=べき等ではない) Reactにおいては、あるコンポーネントに同じprops, State, またはContextが与えられれば、常に同じレンダリング結果を生成する(=べき等である)ことが期待されます。 このルールを守るためには、以下のような点を守る必要があります。 Effectなど、定められた方法以外で副作用を発生させない ( Side effects must run outside of render ルールに準拠する) useEffect などの各種フックには正しいdepsを指定する Props and state are immutable (Propsとstateは不変である) このルールはpropsやStateを直接の変更を禁止しています。 例えば、以下のように props オブジェクトを直接変更している場合はこのルールに違反しています。 function Double ( props ) { props. count *= 2 ; return < span > { props. count } </ span > ; } // 本来は以下のようにすべき // function Double(props) { // return <span>{props.count * 2}</span>; // } また、上記のコードはレンダリングフェーズにおいて引数として渡されたpropsオブジェクトを直接変更している(=副作用を起こしている)ので、 Side effects must run outside of render にも違反しています。 Stateを更新する場合も、変数に代入するのではなく、 useState から返却されたsetter関数を使う必要があります。 let [ isLoading , setIsLoading ] = useState( false ); useEffect(() => { isLoading = true ; // <= これは誤り // 以下のようにすべき // setIsLoading(true); } , [] ); Return values and arguments to Hooks are immutable (フックの戻り値と引数は不変である) フックに引数として渡されたオブジェクトや、フックが返却したオブジェクトを直接変更してはならないというルールです。なぜ変更してはいけないのかというと、そのフックに依存したコンポーネントや別のフックが意図せぬ振る舞いをしてしまう可能性があるためです。 フックに渡す引数については Props and state are immutable ルールと同様に、直接の編集は避けるべきです。 例えば以下のようなフックがあったとします。 export function useFetch ( url , options ) { if (options. headers == null ) options. headers = { 'content-type' : 'application/json' } ; // ここ const [ isLoading , setIsLoading ] = useState( false ); const [ error , setError ] = useState( null ); const [ data , setData ] = useState( null ); useEffect(() => { const ac = new AbortController (); const signal = ac.signal; setIsLoading( true ); fetch (url, options) . then (( res ) => res.json()) . then (( data ) => setData(data)) . catch (( error ) => setError(error)) . finally (() => setIsLoading( false )); } , [ url, options ] ); return { isLoading , error , data } ; } このフックにおける、下記の箇所で Return values and arguments to Hooks are immutable ルールの違反があります。 export function useFetch ( url , options ) { if (options. headers == null ) options. headers = { 'content-type' : 'application/json' } ; // ここ // ... 省略 ... } この場合、引数の options を直接編集するのではなく、新しいオブジェクトを生成すべきです。 if (options. headers == null ) options = { ...options, headers : { 'content-type' : 'application/json' } } ; Values are immutable after being passed to JSX (JSXに渡された値は不変である) これは一見、想像しづらいかもしれないですが、以下のようなコードでこのルールの違反が発生しています。 function Layout ( props ) { const styles = { fontSize : '14px' , width : '100%' } ; const header = < Header styles = { styles } /> ; const main = ( < Main styles = { styles } > { props.children } </ Main > ); styles.fontSize = '12px' ; // => ここ const footer = < Footer styles = { styles } /> ; return ( <> { header } { main } { footer } </> ); } 問題なのは以下の箇所で、 styles オブジェクトは <Header> のpropsとして渡されたあとに直接変更が加えられています。このような場合、意図せぬレンダリング結果を引き起こしてしまうことが考えられます。 const styles = { fontSize : '14px' , width : '100%' } ; const header = < Header styles = { styles } /> ; // ... 省略 ... styles.fontSize = '12px' ; // => ここ const footer = < Footer styles = { styles } /> ; この場合も Return values and arguments to Hooks are immutable ルールなどのケースと同様に、新しいオブジェクトを作成するとよいでしょう。 const styles = { fontSize : '14px' , width : '100%' } ; const header = < Header styles = { styles } /> ; // ... 省略 ... const footerStyles = { ...styles, fontSize : '12px' } ; // 新しいオブジェクトを作る const footer = < Footer styles = { footerStyles } /> ; なぜRules of Reactが重要か? まず、Reactにおいてコンポーネントはどういった記述を避けるべきかがしっかりと定義されたことが挙げられるかと思います。Ruels of Reactに従っておくことでバグが発生しにくいコードを記述するのに役立ち、また今後のReactや周辺エコシステムのアップデートなどにも追従しやすくなることも期待されます。 また、Rules of Reactに従っておくことで、React公式によって開発されている React Compiler による最適化の恩恵を受けやすくなるメリットもあります。 github.com React CompilerはReactで提供されている useCallback や useMemo , React.memo などによる最適化の適用を自動化してくれます。ただし、このReact Compilerがきちんと動作するためには、ReactコンポーネントやフックがRules of Reactに従っていることが重要です。 React Compilerは実験的ツールであるため、現時点での導入はまだ早いとは思いますが、後述する eslint-plugin-react-compiler というESLintプラグインがあるため、まずはこちらの導入を検討すると良いです。 Reactとは何なのか? Rules of Reactの一連のルールについての概要を確認しました。これらの一連のルールに共通する点として「副作用の存在をきちんと意識する」ということがReactにおいて重要なポイントなのではないかと思いました。 Reactが誕生した当初、人気のあったいくつかのフレームワーク (Angular.js v1など)は双方向バインディングという機能を備えていました。この機能はとても便利である一方、複雑化するとレンダリング結果が予測しづらくなりがちであるという課題がありました。Reactではフレームワークレベルで参照透過性を意識して設計されることにより、このレンダリング結果が予測しづらくなる課題を解消することが目的の一つであったのではないかと推測しています。 Reactの特徴としてよく挙げられる宣言的であるという性質はReactにおいて重要な特性の一つではあると思いますが、それはどちらかといえば参照透過性によってもたらされる副次的な要素であるのではないかと個人的には考えています。 また、Reactにおけるコンポーネントの実態はpropsまたはStateを受け取ってVNodeというオブジェクトを返却する関数と考えられます。もしコンポーネントがStateやEffect, Contextなどに依存せず、propsのみに依存しているのであれば、コンポーネントを副作用のない純粋関数として実装することもできます。そういったコンポーネントは副作用もなく特定の文脈などへの依存も少ないため、容易に再利用ができますし、レンダリング結果の予測やテストコードの記述なども容易に行えます。 こういった点などを基に考えると、Reactというのは副作用や参照透過性といったものの存在を念頭において設計することで、高い再利用性やレンダリング結果の予測などを可能とすることを目的としたフレームワークであると考えます。 実践編 Rules of Reactのルールについていくつか紹介しました。具体的にRules of Reactを実際のアプリケーション開発に適用するにはどうすれば良いでしょうか? React公式から推奨されている方法などについて紹介します。 <StrictMode> を有効化する <StrictMode> とはReactフレームワークによって提供されているコンポーネントです。 この <StrictMode> が有効化されると、Reactは意図的にあるコンポーネントを複数回余分にレンダリングしたり、Effectを余分に実行します。 この振る舞いにより、もし Components and Hooks must be idempotent や Side effects must run outside of render などのルールに違反しているコンポーネントやフックが存在する場合に、意図せぬ振る舞いを引き起こす可能性があります。 <StrictMode> は問題のあるコンポーネントやフックを特定するのに役立ち、導入も比較的しやすいと思われるため、ぜひ導入しておくと便利です。 import { StrictMode } from 'react' ; import { createRoot } from 'react-dom/client' ; const root = document . getElementById ( 'root' ) ; createRoot ( root ) . render ( < StrictMode > < App /> </ StrictMode > ) ; eslint-plugin-react-hooks を導入する eslint-plugin-react-hooks についてはおそらく使ったことがある方も多いのではないかと思います。 www.npmjs.com Rules of Reactで紹介されているルールのうち、Rules of Hooksに違反しているコードを検出してくれます。 github.com ESLintプラグインであり導入のハードルなども比較的低いと考えられるため、これについてはぜひ導入をしておくとよいでしょう。 eslint-plugin-react-compiler を導入する eslint-plugin-react-compiler はReact公式によって開発されているESLintプラグインです。 www.npmjs.com React QueryやMUI (Material UI)などの有名なライブラリでもすでに導入されており、ある程度大規模なプロジェクトにおいても少しずつ運用が進められているようです。 github.com github.com このプラグインを設定しておくことで、例えば、コンポーネントやフックからグローバル変数などに対して操作を行おうとすると、以下のようなエラーが発生します。 Writing to a variable defined outside a component or hook is not allowed. Consider using an effect react-compiler/react-compiler 同様に、コンポーネントやフックからグローバル変数に対して再代入を行うと、以下のようなエラーが発生します。 Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) このように eslint-plugin-react-compiler はRules of Reactに違反しているコンポーネントやフックなどのコードを検出してくれます。結構厄介そうなバグとかも拾ってくれそうなので、個人的にはかなり便利なのではないかと感じます。 現時点だとESLint v9のFlat Configにはまだ対応していなさそうなので、もしFlat Configと併用したい場合は @eslint/compat を利用する必要がありそうです。 import { fixupPluginRules } from '@eslint/compat' ; import tseslint from 'typescript-eslint' ; import pluginReact from 'eslint-plugin-react' ; import pluginReactCompiler from 'eslint-plugin-react-compiler' ; export default tseslint.config( ...tseslint.configs.recommended, pluginReact.configs.flat.recommended, // ... 省略 ... { plugins : { 'react-compiler' : fixupPluginRules(pluginReactCompiler) } , rules : { 'react-compiler/react-compiler' : 'error' , } , } , ); React Compilerの導入そのものはまだ早いとは思いますが、 eslint-plugin-react-compiler についてはESLintプラグインであり導入も比較的しやすいとは思うため、もし可能なら今のうちに入れておくのもよいと思います。 おわりに 以上、Rules of Reactを基にReactについて改めて見ていきました。現時点ではドキュメントの分量もそこまで多くはないので、比較的スムーズに読み進めやすいのではないかと思います。最新のドキュメントは以下のリンクから閲覧いただけます。 react.dev また、今回、 eslint-plugin-react-compiler を試してみたのですが、触ってみた感覚としてはとても便利な印象を受けました。コードレビューなどでは気付きにくい厄介な問題なども検出してくれそうなため、もし余裕がありそうなら導入をしてみるとよさそうに感じました。
MiiTel Platform チームの小門です。 2024年9月27日(金)~29日(日)に開催される PyCon JP 2024 に RevComm のエンジニア陶山 嶺と、わたし小門 照太の2名が登壇します イベント概要 https://2024.pycon.jp/ja 公式サイトから引用 PyCon JP は、Python ユーザが集まり、Python や Python を使ったソフトウェアについて情報交換、交流をするためのカンファレンスです。 PyCon JP の開催を通じて、Python の使い手が一堂に集まり、Python にまつわる様々な分野の知識や情報を交換し、新たな友達やコミュニティとのつながり、仕事やビジネスチャンスを増やせる場所とすることが目標です。 日程: 2024年9月27日(金)~29日(日) 会場: TOC有明コンベンションホール 主催: 一般社団法人 PyCon JP Association チケット申し込みはこちらから pyconjp.connpass.com 登壇情報 実例から学ぶ型ヒントの活用手法 近年のPythonは型ヒントの強化が活発で、メジャーアップデートのたびに便利な機能が追加されています。 さらに型ヒントを活用するライブラリやツールも多く登場し、コミュニティからの絶大な人気を集めています。 (中略) Pythonの型ヒントはまだまだ多くの可能性を秘めていると思います。 本セッションを通じて、普段の開発で型ヒントをより便利に活用し、新たなアイディアを自身の手で具体化していきましょう。 日時: 2024年9月28日(土) 12:40 - 13:10 登壇者: Rei Suyama リンク: https://2024.pycon.jp/ja/talk/LXVHNY Rustを活用したPythonライブラリの開発 Python以外の言語で実装された機能(モジュール、クラス、関数)をPythonのライブラリとして使用することが可能です。 有名なものでは Numpy / Pandas は高速化のために主にC言語をベースに実装されています。 最近ではC/C++以外にもRust言語の活用が注目されています。 本セッションでは、Rust を利用してPythonライブラリを開発する利点や手順などを解説します。 また実際にRustが使用されているライブラリの実例を紹介します。 日時: 2024年9月28日(土) 13:30 - 14:00 登壇者: Shota Kokado リンク: https://2024.pycon.jp/ja/talk/7GPRYL おわりに 今回登壇する2名ともに昨年に引き続きの登壇となり、光栄です。 会場でお会いできることを楽しみしております!
概要 こんにちは、RevCommのエンジニア、加藤(涼)です。今回は、私たちが参加した最高のイベントについてお話しします! 2024年08月26日(月)に弊社から4人1チームが AWS Startup GenAI GameDay に挑戦してきました。 普段はそれぞれ異なるプロジェクトに携わっている私たちですが、この日は一丸となってAWSの最新技術に挑戦しました。 この記事ではその参加レポートを共有します! AWS GameDayとは そもそもAWS GameDayとは何かをご紹介します。 AWSのサイトから引用します。 AWS GameDay は、チームベースの環境で、AWS ソリューションを利用して現実世界の技術的問題を解決することを参加者に課題として提示する、ゲーム化された学習イベントです。従来のワークショップとは異なり、GameDay は自由で緩やかな形式で、参加者は固定概念にとらわれずに探索し、考えることができます。 また、AWSでは様々なGameDayが開催されていますが、今回のAWS Startup GenAI GameDayでは、スタートアップ企業向けで、特にGenerative AI に特化した内容でした。 ↓詳しくはこちらのリンクから aws.amazon.com 参加の動機 今回のAWS Startup GenAI GameDayへの参加を決めた理由は主に2つあります。 1. AWS の Generative AI サービスへの興味 MiiTelでは多くのチームが生成AIを活用していますが、自分のチームでもどのように生成AIを取り入れられるか興味がありました。 そのためこのイベントは、最新の AI 技術を実践的に学べる絶好の機会だと考え、迷わず参加を決意しました! 2. AWS GameDay への初挑戦 過去数年間、AWS Summit には参加してきましたが、同時開催の AWS GameDay にはタイミングを逃して参加できずにいました。 毎回キャンセル待ちの長蛇の列を見て、「次こそは!」と思っていたので、今回はその思いを実現できる絶好のチャンスでした! 参加するまでにやったこと 何が出るかは分からなかったですが、GameDayに向けて、以下のような準備を行いました: AWS公式ドキュメント・ブログを読む Generative AI関連サービスについて重点的に学習しました。特に最近だと Amazon Bedrock , その派生の PartyRock あたりがアツいです。 また、Bedrockはlambda, DynamoDBなどの他サービスとの連携が多いです。 AWS の公式ブログ のように構成図が上がっているので事前に読むようにしてみました。 コミュニケーションツールの準備 当日のスムーズな情報共有のため、Slackのプライベートチャンネルを作成し、URLやコード共有を素早くできるように準備しておきました。 当日の様子 当日は日本から8社が参加しました。 到着してから知ったのですが、実はインド・韓国からも同時参加していて、計26社がこのGameDayに参加していたそうです。 ルールは各国のスタートアップとリアルタイムでポイントを競うというもので、そのポイントはダッシュボードで確認できるようになっています。 私たちのチーム名は「JP_Under30」にしました。参加したメンバーが全員20代なので、この名前にしました笑 そしてGameDayの開始です。 (AWSの公式ブログはこちら!) aws.amazon.com 最初は何をして良いか分からず、それぞれのメンバーで課題を分担することにしました。最初の30分ぐらいですでに大量のポイントが入っているチームもあり、この時は少し焦っていましたね。 開始約30分後の順位 順調に進めていき、ダッシュボードを眺めたときがこちら 途中で確認した順位 下がっている。。19位。。やはり、他のチームもどんどんポイントを獲得しているようです! 何とか色々な課題を解いてポイントを獲得して残り10分の時 残り10分での順位 6位まで上がっていました!すごい! この後順位が前後しましたが、最終的に全体で5位、国内で3位という結果になりました。 ということで国内で入賞しました! 入賞賞品として、AWSのBuildersCardsというものを頂きました!これは、AWSのサービスや構成を学ぶことができるカードゲームです。嬉しいですね!( AWS BuilderCards ) 記念写真 記念写真 (左から、瀧山、中島、加藤、ホセ、) 感想 今回のAWS Gamedayを通じて、新しい技術を試すことの楽しさや、チームで課題を解決することの重要性を改めて実感しました。 また、普段は異なるプロジェクトに取り組んでいるメンバーと協力することで、新しい視点やアイデアを得ることができました。限られた時間内で効率的に作業を進めるためには、各メンバーの強みを活かしつつ、互いにサポートし合うことが不可欠でした!みなさんありがとうございます!! まとめ 今回のAWS Startup GenAI GameDayを通じて、多くのことを学び、成長することができました! 結果は全体で5位、国内で3位という素晴らしい成績を収めることができました! チームワークの重要性、新しい技術への挑戦、そして時間管理の大切さ - これらすべてが、今後の仕事や技術開発に活かしていきたいと思います!!
概要  こんにちは、RevCommでMiiTelの音声解析機能に関する研究開発を担当している石塚です。 前回のRevComm Tech Blog にて、2023年時点でSOTAの精度であったE-Branchformer[ 1 ]を利用して日本語の音声認識モデルを構築する記事について書きました。  前回の実験において、E-Branchformerで構築したモデルは、精度ではConformerで構築したモデルより優れていましたが、スピードはConformerで構築したモデルよりも少し遅いものとなっていました。音声認識システムの実運用を考えると、音声認識のスピードは非常に重要です。  そこで今回は、高速な非自己回帰型のアーキテクチャでE-Branchformerの音声認識モデルを構築し、どの程度のスピードで音声認識可能かを確かめたいと思います。 石塚賢吉(いしづか けんきち) プリンシパルリサーチエンジニア。筑波大学大学院博士後期課程卒業。博士(工学)。日本HP株式会社にて通信事業者向けのシステム開発、株式会社ドワンゴで全文検索システムの開発などに従事。2019年12月、株式会社RevComm入社。音声認識、音声感情認識、全文検索システムの研究開発を行なっている。 → 過去記事一覧 非自己回帰型の音声認識モデルについて  E2E音声認識モデルは、大きく分けると自己回帰モデルと非自己回帰モデルに分類できます。前回の実験で構築したモデルは、 EncoderにE-Branchformer、DecoderにTransformerを用いたものであり、Attention Encoder-Decoderと呼ばれる自己回帰型に分類されるモデルでした。このタイプのモデルでは、下記の図のように過去の出力トークンに基づいて現在の出力トークンを生成するため、N個のトークンの生成のためにN回の計算が必要になります。 一方、非自己回帰型の音声認識モデルでは、音響特徴量のシーケンスからテキストトークンのシーケンスを直接生成し、一定の計算コストで処理できるため、高速に推論することが可能です。ESPnetでは、Mask-CTC[ 2 ]と呼ばれる非自己回帰型の音声認識モデルを利用することができます。  Mask-CTCでは、下記のように入力された音声からEncoderで音響特徴量を抽出し、Connectionist Temporal Classification(CTC)という手法で、音響特徴量から発話テキストを直接推定します。CTCでは、Blankラベルトークンを導入することで、長さが大きく異なる音響特徴量シーケンスとテキストのトークンのシーケンスとの対応関係を表現しています。さらにMask-CTCでは、CTCの出力トークンのうち信頼度の低いトークンをマスキングし、TransformerのMasked Language Model(MLM)のMask-Predictionを用いて誤りを修正することで、音声認識精度を改善します。Mask-CTCのアルゴリズムの詳細については、 論文 を参照ください。  今回は、E-BranchformerでMask-CTCの非自己回帰型の音声認識モデルを構築したいので、EncoderにE-Branchformer、DecoderにCTC、およびMLM Decoderを用いて、音声認識モデルを構築します。 実験方法 前回 と同様にCSJデータセットでモデルを構築します。 E-Branchformerは ESPNetのv.202301 から利用できる状態になっていますが、Mask-CTCと組み合わせて利用するレシピはまだ存在しない状態です。そこで、WSJのMask-CTCのレシピの学習の 設定ファイル と、LibriSpeechのE-Branchformerのレシピの学習の 設定ファイル を元に、EncoderにE-Branchformer、DecoderにCTCとMLMを利用するモデルの学習の設定ファイルを作成します。今回作成した学習の設定ファイルは下記になります。 train_asr_e_branchformer_small_mask_ctc.yaml batch_type: folded batch_size: 32 accum_grad: 8 max_epoch: 300 patience: none init: none best_model_criterion: - - valid - cer_ctc - min keep_nbest_models: 10 # specify model type as "maskctc" model: maskctc model_conf: ctc_weight: 0.3 lsm_weight: 0.1 length_normalized_loss: false use_amp: true unused_parameters: true num_workers: 4 encoder: e_branchformer encoder_conf: output_size: 256 attention_heads: 4 attention_layer_type: rel_selfattn pos_enc_layer_type: rel_pos rel_pos_type: latest cgmlp_linear_units: 3072 cgmlp_conv_kernel: 31 use_linear_after_conv: false gate_activation: identity num_blocks: 12 dropout_rate: 0.1 positional_dropout_rate: 0.1 attention_dropout_rate: 0.1 input_layer: conv2d layer_drop_rate: 0.1 linear_units: 1024 positionwise_layer_type: linear macaron_ffn: true use_ffn: true merge_conv_kernel: 31 # Masked Language Model (MLM)-based decoder decoder: mlm decoder_conf: attention_heads: 4 linear_units: 2048 num_blocks: 6 dropout_rate: 0.1 positional_dropout_rate: 0.1 self_attention_dropout_rate: 0.1 src_attention_dropout_rate: 0.1 optim: adam optim_conf: lr: 0.002 weight_decay: 0.000001 scheduler: warmuplr scheduler_conf: warmup_steps: 15000 num_att_plot: 0 specaug: specaug specaug_conf: apply_time_warp: true time_warp_window: 5 time_warp_mode: bicubic apply_freq_mask: true freq_mask_width_range: - 0 - 27 num_freq_mask: 2 apply_time_mask: true time_mask_width_ratio_range: - 0. - 0.05 num_time_mask: 5 ESPNet v.202301の学習環境とCSJのセットアップが完了した状態から、E-Branchformerを用いた音声認識モデルを構築する手順は下記のとおりです。 作成した学習の設定ファイルtrain_asr_e_branchformer_small_mask_ctc.yamlを CSJレシピのconfディレクトリ に配置する CSJレシピの学習スクリプトの学習設定ファイルの参照先 をasr_config=conf/ train_asr_e_branchformer_small_mask_ctc.yaml にする Mask-CTCの推論の設定ファイル を CSJレシピのconfディレクトリ の配下にコピーする。 CSJレシピの推論設定ファイルの参照先 をinference_config=conf/inference_asr_maskctc.yaml にする。 ./asr.sh で --use_maskctc true を設定して run.sh を実行する 得られたモデルの音声認識の精度とスピード モデルの学習  ABCIのrt_Fインスタンス (NVidia V100*4)でCSJの学習データを用いて50エポックまで音声認識モデルの学習を行ったところ、CSJのValidセットのCharacter Error Rate(CER; 文字誤り率)とEpochの関係は下記のようになりました。 音声認識精度の評価   前回 学習した、EncoderがE-BranchformerでDecoderがTransformerのモデルと、今回学習したEncoderがE-BranchformerでDecoderがCTC・MLMのモデルで、CSJのテストセットを音声認識したときのCERを下記の表に示します (%)。Decoderの列が CTC の行は、CTCの出力トークンそのままの精度で、 CTC+MLM の行は、CTCの出力トークンをMLMのMask-Predictionで修正した時の精度です。  なお、比較対象として、 Mask-CTCの論文 にあるEncoderがTransformerでDecoderがCTCとMLMの構成のモデルを50Epochまで学習したものと、EncoderとDecoderの両方がTransformerの プリトレインドモデル を用意しました。なお、DecoderがCTC・MLMのモデルは、 Mask-CTCの論文 に合わせて、Encoderの中間層のニューロンの数が256となっていることにご注意ください。また、DecoderがTransformerの音声認識モデルでは、言語モデルは利用していません。 Encoder Decoder Type CER(eval1) CER(eval2) CER(eval3) E-Branchformer (Output:512) Transformer 自己回帰型 3.8 2.9 3.2 E-Branchformer (Output:256) CTC 非自己回帰型 4.5 3.2 3.8 E-Branchformer (Output:256) CTC+MLM (p=0.9, it=5) 非自己回帰型 4.5 3.2 3.8 Transformer (Output:512) Transformer 自己回帰型 4.9 3.8 3.8 Transformer (Output:256) CTC 非自己回帰型 6.2 4.6 5.4 Transformer (Output:256) CTC+MLM (p=0.9, it=5) 非自己回帰型 6.2 4.4 5.3  表を見ると、E-Branchformer の非自己回帰型の音声認識モデルは、精度において自己回帰型のE-Branchformerの音声認識モデルには及ばないようです。しかし、E-Branchformer の非自己回帰型の音声認識モデルは、Transformerの自己回帰型の音声認識モデルよりも良い精度となっています。なお、今回の実験では、EncoderがTransformerの非自己回帰型の音声認識モデルでは、MLMによる修正で精度が上がりましたが、EncoderがE-Branchformerの非自己回帰型の音声認識モデルについては、MLMによる修正を行なっても精度が向上しませんでした。 音声認識スピードの評価  次に、CSJのeval1からeval3のテストセットをCPUまたはGPUで音声認識するときのスピードを下記のReal Time Factor (RTF) と、Inverse Real Time Factor (iRTF) の指標で確認しました。iRTFは、1秒間で何秒の長さの音声を認識できるかを表す指標です。 CPUでのデコードについて  AWSのc5.xlargeのCPUで1Threadでデコードした時のRTFとiRTFを下記の表に示します。なお、DecoderがTransformerの自己回帰型のモデルでは、beam size=2でデコードしています。 Encoder Decoder Type RTF iRTF E-Branchformer (Output:512) Transformer 自己回帰型 2.291 0.436 E-Branchformer (Output:256) CTC 非自己回帰型 0.664 1.506 E-Branchformer (Output:256) CTC+MLM (p=0.9, it=5) 非自己回帰型 0.805 1.242 Transformer (Output:512) Transformer 自己回帰型 0.383 2.611 Transformer (Output:256) CTC 非自己回帰型 0.031 32.258 Transformer (Output:256) CTC+MLM (p=0.9, it=5) 非自己回帰型 0.042 23.810  EncoderがE-Branchformerの自己回帰型の音声認識モデルの音声認識スピードは、RTFの値が1以上となってしまいました。また、EncoderがE-Branchformerの非自己回帰型の音声認識モデルは、EncoderがTransformerの非自己回帰型の音声認識モデルよりも音声認識スピードが遅いようでした。E-Branchformerのエンコーダは、CPUでは計算処理が重いようです。 GPUでのデコードについて  NVIDIA A10を搭載するAWSのg5.xlargeインスタンスを用いて、デコードした時のRTFを下記の表に示します。なお、バッチデコードはしていません。 Encoder Decoder Type RTF iRTF CPUに対するスピード倍率 [iRTF(GPU)/iRTF(CPU)] E-Branchformer(Output:512) Transformer 自己回帰型 0.235 4.255 9.749 E-Branchformer(Output:256) CTC 非自己回帰型 0.011 90.909 60.364 E-Branchformer(Output:256) CTC+MLM (p=0.9, it=5) 非自己回帰型 0.013 76.923 61.923 Transformer(Output:512) Transformer 自己回帰型 0.198 5.051 1.934 Transformer(Output:256) CTC 非自己回帰型 0.009 111.111 3.444 Transformer(Output:256) CTC+MLM (p=0.9, it=5) 非自己回帰型 0.011 90.909 3.818  GPUによるデコードでは、E-Branchformerの非自己回帰型の音声認識モデルの音声認識スピードがCPUの時と比べ大幅(約62倍)に向上し、Transformerの非自己回帰型の音声認識モデルに近いスピードとなっています。非自己回帰型の音声認識モデルは、GPUで音声認識した時に大きくスピードが向上するようです。一方で、自己回帰型の音声認識モデルは、GPUで音声認識しても、非自己回帰型の音声認識モデルの時ほど音声認識スピードが向上しないようです。原因は深く追っていませんが、TransformerのDecoderでの自己回帰的な処理とBeamsearchなどのポストプロセスの処理がボトルネックになっているものと思われます。 まとめ  本記事では、高速な非自己回帰型の音声認識モデルである、E-BranchformerのMaskCTCの音声認識モデルを構築し、音声認識精度とスピードを確認しました。E-BranchformerのMaskCTCの音声認識モデルは、非自己回帰型の音声認識モデルでありながら、Transformerの自己回帰型の音声認識モデルよりも精度が高く、GPUを用いた時に高速に音声認識できることがわかりました。  なお、本記事では触れませんでしたが、今回構築した非自己回帰型の音声認識モデルをONNXなどの推論エンジンの形式に変換することで、さらに音声認識スピードを向上できるようでした。また、今回の実験でのGPUによる音声認識はバッチデコードになっていませんでしたが、おそらくバッチデコードを行うことで、さらなる高速化が期待できます。 参考文献 [1] Kim, K., Wu, F., Peng, Y., Pan, J., Sridhar, P., Han, K. J., & Watanabe, S. (2023). E-Branchformer: Branchformer with Enhanced Merging for Speech Recognition. Proc. IEEE Spoken Language Technology Workshop (SLT), 84–91. [2] Yosuke Higuchi, Shinji Watanabe, Nanxin Chen, Tetsuji Ogawa, Tetsunori Kobayashi, Mask CTC: Non-Autoregressive End-to-End ASR with CTC and Mask Predict, Proc. INTERSPEECH 2020, 3655-3659
はじめに こんにちは。ここ最近、React v19が注目を集めているように感じます。 最近ではついにReact v19のRCバージョンもリリースされました。 Just published React 19.0.0-rc.0. This is the exact build we'll release as 19.0, unless an issue is reported that requires a breaking change. Thank you to everyone who helped us get the release into shape! — Andrew Clark (@acdlite) 2024年6月3日 そう遠くないうちにReact v19がリリースされる可能性もありそうです。そこで、React v19のリリースによる周辺ライブラリへの影響などについて、弊社で採用しているライブラリを中心に調査をしてみました。この記事ではその内容をまとめます。 ESLint React Compiler向けのESLintプラグインが公開されているようです。 www.npmjs.com React Compilerとは? React CompilerとはReactアプリケーションの最適化をおこなってくれるツールです。このReact Compilerの利用には、現状ではReact v19のRCバージョンが必要とされます。 ※React Compilerはまだ実験的ツールであり、今後、大きな変更などが入る可能性もあります。 react.dev Reactには useMemo や React.memo などの最適化方法が提供されていますが、正しく扱うのは難しかったり、また、適用をし忘れることなどもあります。React Compilerはこれらの適用を自動化してくれます。 ただし、React Compilerが動作するためには、コードベースが Rules of React に従っている必要があります。Rules of Reactとは自然なReactコードを記述するために従うべきとされているルールです。 react.dev eslint-plugin-react-compiler について Reactの公式から eslint-plugin-react-compiler というプラグインが公開されています。このプラグインはRules of Reactに従っていないコードを検出してくれます。そのため、このプラグインを導入しておくことで、将来的にReact Compilerの導入などがスムーズに行いやすくなりそうです。React Compilerそのものはまだexperimentalなので現時点での導入は避けた方が安全だと思いますが、この eslint-plugin-react-compiler については比較的試しやすいとも思うので、早いタイミングで導入してしまってもよいのかもしれません。 ( 注意 ) Reactの公式ドキュメントでは、「 eslint-plugin-react-compiler によってRules of Reactへの違反が検出されたとしても急いで直す必要はない」ということが言及されています。少しずつ段階的に修正をしていくのがよさそうです。 You don’t have to fix all eslint violations straight away. You can address them at your own pace to increase the amount of components and hooks being optimized, but it is not required to fix everything before you can use the compiler. TanStack Query React v19への対応状況について 以下のPRでReact v19のサポートに向けた対応が入っており、すでにリリースもされているようです。( v5.39.0 ) github.com また、上記のPRでは eslint-plugin-react-compiler も導入されており、React Compilerに向けた準備なども既に考慮されているようです。 TanStack Queryについては既にかなり準備が進んでいそうな印象です。 React v19による影響について github.com 上記のDiscussionページによると、React v19で導入予定の use との統合のため useQuery の戻り値に promise プロパティを追加することなどが検討されているようです ( https://github.com/TanStack/query/discussions/7074#discussioncomment-8748245 ) const { promise } = useQuery ( options ) ; const data = use ( promise ) ; React Router React v19による影響について React v19におけるtransitionでのasync関数のサポートに合わせて、React Routerでは navigate や submit などの各種APIが Promise を返すようにする対応が検討されているようです。 github.com github.com これらの変更はReact Routerのv7で入る想定のようです。 React Hook Form React v19への対応状況について peerDependencies でReact v19を許可する対応が導入されており、すでにReact v19への対応が視野に入れられているようです。( v7.52.0 ) github.com React v19による影響について React v19で導入予定のAPIとの統合に関するDiscussionページが作成されています。 github.com まだ具体的な計画や構想などはなさそうなのですが、 https://github.com/orgs/react-hook-form/discussions/11832#discussioncomment-9450350 のコメントでは以下の動画が紹介されています www.youtube.com github.com この動画では useActionState とReact Hook Formを統合する方法などについて紹介されています。現状、ある程度工夫が必要そうな状況のようです。先ほどのDiscussionページにおいてもReact Hook Formからも何かしらの解決策があるとよさそうというような意見はありますが、まだ現状ではどうなるのかはわかりません。 @sentry/react React v19による影響について React v19サポートに向けたissueが作成されています。 github.com React v19では onCaughtError や onUncaughtError , onRecoverableError などのオプションが createRoot や hydrateRoot に追加されます。これらのオプションでの利用が想定された Sentry.reactErrorHandler() というAPIが追加されています ( v8.6.0 ) import { createRoot } from 'react-dom/client' ; import * as Sentry from '@sentry/react' ; const root = createRoot( document . getElementById ( 'app' ), { onCaughtError : Sentry.reactErrorHandler(), onUncaughtError : Sentry.reactErrorHandler(), onRecoverableError : Sentry.reactErrorHandler(), } , ); onCaughtError や onUncaughtError は全てのコンポーネントに対して適用されるため、もしよりきめ細かな制御が必要な際は、既存の Sentry.ErrorBoundary の利用が推奨されるようです。 github.com sentry.io React Testing Library React v19への対応状況について すでにReact v19のCanary版を使ってテストが行われているようです。 github.com また、React v19のアップグレードガイドには react-dom/test-utils が react へ移動されると記述されています。React Testing Libraryはこの変更の影響を受けそうです。 react.dev この変更についても既に対応が行われているようです。 github.com github.com Recoil React v19への対応状況について 以下のissueでReact v19サポートに関する要望があります github.com ただし、対応についてはまだ進められているわけではなさそうです。このissueで積極的に発言されている wojtekmaj 氏によると、やや開発も停滞しており別の選択肢への移行も考慮するよう提案されています。やや芳しくはなさそうな状況には見えました…( https://github.com/facebookexperimental/Recoil/issues/2318#issuecomment-2120383312 ) React Redux React v19への対応状況について 以下のPRでReact v19に関する対応が進められていそうです。PR上のTODOリストについては一通り消化されている状態のようなので、着実に対応が進められているような印象です。 github.com MUI (Material UI) React v19への対応状況について React v19の対応に関するissueが先月に公開されています。React v19の型定義との互換性の改善やReact Compilerによる最適化を活用するためにRules of Reactへの対応などが計画されているようです。 github.com eslint-plugin-react-compiler についてはすでに導入( #42555 )されており、少しずつ対応が進められていく想定のようです。 github.com @apollo/client React v19への対応状況について 最近リリースされた v3.10.5 から、React v19を使ったテストが開始されています。 github.com また、 ロードマップ によると、React Compilerのサポートに向けて useQuery と useSubscription を書き直す計画があるようです おわりに 今回、調査にあたって色々と調べて見たのですが、その中でも特に eslint-plugin-react-compiler は便利そうに感じました。Rules of Reactにできる限り準拠をしておくことで、将来的なReactに導入される様々な変更にも追従しやすくなることが考えられます。またESLintプラグインとして提供されているため、比較的、導入などのハードルが低めなことも魅力的に感じました。そのため、早めのタイミングで導入を検討してみてもよいのかもしれないと思いました。 この記事で紹介したもの以外にも、様々なライブラリでReact v19へ向けた対応や新機能の追加などが想定されます。より開発に便利な機能などが追加される可能性もあるため、今後も引き続き注目していきたいです。 参考 github.com github.com react.dev react.dev zenn.dev
Analytics Teamの山内健二です。RevCommの解析基盤に導入されているAmazon EKSクラスタでBlue/Greenアップグレードを導入し、合わせて今後の工数削減のために自動化を行ったので、その内容をご紹介します。 RevCommの解析基盤の概要 アップグレードの内容の前に、簡単にRevCommの解析基盤を簡単に紹介します。RevCommはMiiTel、MiiTel Meetingsなど、複数のプロダクトを提供しておりますが、文字起こしなどの解析を実施する基盤は共通して単一のものとなっています。 この解析基盤は単一のアプリケーションから動いているのではなく、音声認識など解析機能等の単位で分割された複数のアプリケーションから構成されており、現在これらが1つのEKSクラスタの中にホストされています。 EKSクラスタを含めたAWSリソースや、AWS Load Balancer ControllerなどのミドルウェアはTerraformによりIaC化して管理しています。また、クラスタ内で稼働しているアプリケーション用のKubernetesマニフェストは別途リポジトリを用意して管理しており、クラスタ内で動いているArgo CDでmainブランチを参照させてデプロイしています(Pull型のGitOps)。 EKSのアップグレード方針 RevCommの解析基盤では、EKSを導入してからこれまでin-place方式、すなわち既存クラスタのバージョンを直接アップグレードしていました。前述したようにRevCommの解析基盤はIaC化しているため、いくつかの設定値の変更のみで簡便に行えるのですが、欠点もあり、今回Blue/Green方式でのアップグレードを導入することにしました。 具体的にはin-place方式では EKS Best Practices Guide でも解説があるように順次ノードグループ等のリソースをアップグレードします。そのため、サービスのダウンタイムが発生する可能性があり、また、アップグレード後に問題が発生しても切り戻しができません。さらに、クラスタ内で稼働しているノード数の増加に伴い、アップグレード自体の時間も最大数時間に及んでいました。 一方、Blue/Green方式では現在のクラスタとは別に新しいバージョンのクラスタを用意し、こちらへトラフィックを順次誘導していきます。問題が起きないことを確認してから古いバージョンのクラスタを破棄するため、前述したような問題は起こりません。しかし、新しいクラスタの用意が必要なため、その分の工数が必要です。今回RevCommでは工数を考慮してもin-place方式で述べた欠点の克服にメリットがあると考え、Blue/Green方式を採用しました。 実装方法 クラスタ切り替えの概要 今回の実装にあたっては、構成が類似していることもあり Amazon EKS Blueprints for TerraformのBlue Green Migration を参考にしました。 基本的には下記の流れで行います。現行クラスタがバージョン1.25で、新規クラスタをバージョン1.28で実施する際の例を図で示しています。 新規のクラスタ作成とミドルウェアのインストールをTerraform側で実施 新規クラスタにArgo CDでアプリケーションをデプロイ マニフェスト側の変更 external-dnsを利用し、Ingressのアノテーション経由でRoute 53の加重ルーティングによりトラフィックを分配 set-identifierとaws-weightを設定します これらの変更はマニフェストのリポジトリのフィーチャーブランチとして作成し、新規クラスタ側のArgoCDは一旦そちらを参照するようにします。現行クラスタはmainブランチを参照したままです。 新規クラスタでの挙動が問題ないことを確認したら現行クラスターを削除 前述したフィーチャーブランチをmainブランチへマージし、新規クラスタのArgoCDもmainブランチを参照するようにします 自動化 これらの流れはTerraformでのvariableの値やマニフェストの一部の調整のみで実施できるものの、手順が複数工程にまたがることもあり、オペレーションミスや属人化を防ぐためにGitHub Actionsなどにより極力自動化させました。具体的に実施した自動化は、主にクラスタ作成・削除の2つです。図に流れの概要を示しています。 前提として、解析基盤のTerraformのコードは図に示すように各環境(開発、ステージング、本番)ごとでbackendとvariableを管理するためのディレクトリ(以下単に環境設定と呼びます)をvarsディレクトリ以下に配置しています。各環境設定はcluster-YYYYMMのように、環境ごとの識別子をつけて管理しています。例えば図左ではcluster-202403が存在しています。また、現行クラスタ用の環境設定は、図左のclusterのようにシンボリックリンクで指すようにしています。 ここで新規クラスタを作るとき、新しい環境設定の生成・Terraformコードの適用によるクラスタ作成・シンボリックリンクの張替えが必要になります。逆に現行クラスタを削除する場合は、現行クラスタからのアプリケーションとミドルウェアの削除・現行クラスタの削除・現行クラスタの環境設定の削除を行う必要があります。 これらを手作業で行うのはオペレーションミスを誘発するので、GitHub Actionsにて識別子やバージョンを入力として行えるようにしました。 このような自動化により、クラスタの作成から切り替え、削除までをローカルでの手作業なしにできるようになり、時間的にも新規クラスタとそこへのアプリケーションデプロイまで数十分でおこなえるようになりました。 まとめ 今回はRevCommの解析基盤のEKSクラスターのアップグレード方式をBlue/Greenに変更した際の紹介でした。 in-place方式と比べて工数を悪化させないための自動化や手順の確立は必要でしたが、IaC化の素地もあったため、メリットを最大限享受した上でBlue/Green方式を導入できたと感じています。解析基盤自体が、ステートフルな要素が少なく、アプリケーション側で考慮するべきことが少なかったのも親和性がありました。参考になれば幸いです。
Analytics Teamの山内健二です。RevCommの解析基盤に導入されているAmazon EKSクラスタでBlue/Greenアップグレードを導入し、合わせて今後の工数削減のために自動化を行ったので、その内容をご紹介します。 RevCommの解析基盤の概要 アップグレードの内容の前に、簡単にRevCommの解析基盤を簡単に紹介します。RevCommはMiiTel、MiiTel Meetingsなど、複数のプロダクトを提供しておりますが、文字起こしなどの解析を実施する基盤は共通して単一のものとなっています。 この解析基盤は単一のアプリケーションから動いているのではなく、音声認識など解析機能等の単位で分割された複数のアプリケーションから構成されており、現在これらが1つのEKSクラスタの中にホストされています。 EKSクラスタを含めたAWSリソースや、AWS Load Balancer ControllerなどのミドルウェアはTerraformによりIaC化して管理しています。また、クラスタ内で稼働しているアプリケーション用のKubernetesマニフェストは別途リポジトリを用意して管理しており、クラスタ内で動いているArgo CDでmainブランチを参照させてデプロイしています(Pull型のGitOps)。 EKSのアップグレード方針 RevCommの解析基盤では、EKSを導入してからこれまでin-place方式、すなわち既存クラスタのバージョンを直接アップグレードしていました。前述したようにRevCommの解析基盤はIaC化しているため、いくつかの設定値の変更のみで簡便に行えるのですが、欠点もあり、今回Blue/Green方式でのアップグレードを導入することにしました。 具体的にはin-place方式では EKS Best Practices Guide でも解説があるように順次ノードグループ等のリソースをアップグレードします。そのため、サービスのダウンタイムが発生する可能性があり、また、アップグレード後に問題が発生しても切り戻しができません。さらに、クラスタ内で稼働しているノード数の増加に伴い、アップグレード自体の時間も最大数時間に及んでいました。 一方、Blue/Green方式では現在のクラスタとは別に新しいバージョンのクラスタを用意し、こちらへトラフィックを順次誘導していきます。問題が起きないことを確認してから古いバージョンのクラスタを破棄するため、前述したような問題は起こりません。しかし、新しいクラスタの用意が必要なため、その分の工数が必要です。今回RevCommでは工数を考慮してもin-place方式で述べた欠点の克服にメリットがあると考え、Blue/Green方式を採用しました。 実装方法 クラスタ切り替えの概要 今回の実装にあたっては、構成が類似していることもあり Amazon EKS Blueprints for TerraformのBlue Green Migration を参考にしました。 基本的には下記の流れで行います。現行クラスタがバージョン1.25で、新規クラスタをバージョン1.28で実施する際の例を図で示しています。 新規のクラスタ作成とミドルウェアのインストールをTerraform側で実施 新規クラスタにArgo CDでアプリケーションをデプロイ マニフェスト側の変更 external-dnsを利用し、Ingressのアノテーション経由でRoute 53の加重ルーティングによりトラフィックを分配 set-identifierとaws-weightを設定します これらの変更はマニフェストのリポジトリのフィーチャーブランチとして作成し、新規クラスタ側のArgoCDは一旦そちらを参照するようにします。現行クラスタはmainブランチを参照したままです。 新規クラスタでの挙動が問題ないことを確認したら現行クラスターを削除 前述したフィーチャーブランチをmainブランチへマージし、新規クラスタのArgoCDもmainブランチを参照するようにします 自動化 これらの流れはTerraformでのvariableの値やマニフェストの一部の調整のみで実施できるものの、手順が複数工程にまたがることもあり、オペレーションミスや属人化を防ぐためにGitHub Actionsなどにより極力自動化させました。具体的に実施した自動化は、主にクラスタ作成・削除の2つです。図に流れの概要を示しています。 前提として、解析基盤のTerraformのコードは図に示すように各環境(開発、ステージング、本番)ごとでbackendとvariableを管理するためのディレクトリ(以下単に環境設定と呼びます)をvarsディレクトリ以下に配置しています。各環境設定はcluster-YYYYMMのように、環境ごとの識別子をつけて管理しています。例えば図左ではcluster-202403が存在しています。また、現行クラスタ用の環境設定は、図左のclusterのようにシンボリックリンクで指すようにしています。 ここで新規クラスタを作るとき、新しい環境設定の生成・Terraformコードの適用によるクラスタ作成・シンボリックリンクの張替えが必要になります。逆に現行クラスタを削除する場合は、現行クラスタからのアプリケーションとミドルウェアの削除・現行クラスタの削除・現行クラスタの環境設定の削除を行う必要があります。 これらを手作業で行うのはオペレーションミスを誘発するので、GitHub Actionsにて識別子やバージョンを入力として行えるようにしました。 このような自動化により、クラスタの作成から切り替え、削除までをローカルでの手作業なしにできるようになり、時間的にも新規クラスタとそこへのアプリケーションデプロイまで数十分でおこなえるようになりました。 まとめ 今回はRevCommの解析基盤のEKSクラスターのアップグレード方式をBlue/Greenに変更した際の紹介でした。 in-place方式と比べて工数を悪化させないための自動化や手順の確立は必要でしたが、IaC化の素地もあったため、メリットを最大限享受した上でBlue/Green方式を導入できたと感じています。解析基盤自体が、ステートフルな要素が少なく、アプリケーション側で考慮するべきことが少なかったのも親和性がありました。参考になれば幸いです。
バックエンドエンジニアの小門です。 この記事ではグローバルインタプリタロック (GIL) が解消されたPythonを動かしてみた検証の方法と結果について書きます。 なおGIL自体の説明や詳しい仕組みについてこの記事ではほとんど説明しないのでご了承ください。 準備として開発バージョンを取得してソースコードからビルドし、ビルド成果物のPythonランタイムを使って検証します。 追記: 2024/6/14 時点で最新の 3.13.0 beta2 を使ってベンチマークを再疎検証しました。また、一部の内容の訂正を行いました。 準備(ビルド) Pythonにおける「GIL廃止」の第一歩として、CPython本家のリポジトリにおいてGILを無効化できるようにするための修正が2024年3月12日mainブランチへマージされました。 gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 #116338 また同日、上記の変更を取り込んだ開発バージョンが v3.13.0a5 としてリリースされました。 https://www.python.org/downloads/release/python-3130a5/ https://github.com/python/cpython/releases/tag/v3.13.0a5 まだ開発途中のアルファ版ですが、今回はこのバージョンを使って検証していきます。 なお筆者の動作環境は以下の通りです。 CPU: AMD Ryzen 7 3700X(8コア) OS: Ubuntu 22.04 (on WSL2 / Windows10) gcc: 11.4.0 また、本記事の手順ではソースコードをビルドするためのツール群が必要になります。 お使いの環境に応じて必要な準備をしてください。 参考: Python Developer's Guide - Setup and building ビルド/インストール手順 GILを無効化するにはビルド時にオプションを指定しておく必要があります。 オプションは --disable-gil とのこと。分かりやすいですね。 https://github.com/python/cpython/blob/076d169ebbe59f7035eaa28d33d517bcb375f342/configure#L1815-L1816 一連のコマンド手順は以下のようになります。 $ pwd /home/skokado/playground-py313 $ # インストール用ディレクトリを作成 $ mkdir -p local/python-3. 13 $ # ソースコードを取得 $ wget https://www.python.org/ftp/python/ 3 . 13 . 0 /Python-3. 13 .0a5.tgz $ tar xf Python-3. 13 .0a5.tgz $ cd Python-3. 13 .0a5/ $ # オプションの確認 $ ./configure --help | grep gil --disable-gil enable experimental support for running without the $ # ビルド、インストール $ ./configure --disable-gil --prefix $( pwd ) /../local/python -3 . 13 && make install checking build system type ... x86_64-pc-linux-gnu checking host system type ... x86_64-pc-linux-gnu checking for Python interpreter freezing... ./_bootstrap_python ... ( 略 ) Successfully installed pip-24. 0 $ # インストールできたことを確認 $ cd ../local/python -3 . 13 $ ./bin/python3. 13 -VV Python 3 . 13 .0a5 ( main, Mar 14 2024 , 18:37:25 ) [ GCC 11 . 4 . 0 ] ベンチマーク検証 GILはCPUバウンドなマルチスレッド処理において実行可能なスレッドが制限されるものです。 したがってマルチスレッド処理を行うスクリプトで実行結果を比較してみます。 検証スクリプトは以下です。 # test_gil.py from concurrent.futures import ThreadPoolExecutor import time import math def get_primes ( max : int ) -> list [ int ]: # (あえて低速な) n以下の素数一覧を返す関数 if max < 2 : raise ValueError () primes = [ 2 ] for n in range ( 3 , max + 1 ): is_prime = True for i in range ( 2 , int (math.sqrt(n)) + 1 ): if n % i == 0 : is_prime = False break if is_prime: primes.append(n) return primes if __name__ == "__main__" : print ( "concurrency,time" ) for concurrency in range ( 1 , 10 + 1 ): start = time.monotonic() with ThreadPoolExecutor(max_workers=concurrency) as executor: futures = [executor.submit(get_primes, 500000 ) for _ in range (concurrency)] for f in futures: f.result() end = time.monotonic() duration = end - start print (f "{concurrency},{duration:.2f}" ) 「CPUバウンド処理」として素数判定をメインにした関数をThreadPoolExecutorでマルチスレッド処理する ※引数 max は筆者の環境で1、2秒程度かかる値を選択 concurrency で指定されたスレッド数分だけ get_primes を並列に起動する concurrency を1から10まで変化させて所要時間を計測する ※それぞれ3回ずつ実行し、平均時間を取得 ちなみに、上記でビルドしたv3.13.0a5において実際の処理でGILを無効にするには環境変数 PYTHON_GIL=0 とともに実行する必要があります。 $ PYTHON_GIL = 0 ./bin/python3. 13 test_gil.py 結果 比較対象は以下の通りです。 v3.12.2 : 執筆時点の最新リリースバージョン v3.13.0a5 : "--disable-gil" オプション 無し でビルドしたランタイム v3.13.0a5 & --disable-gil : "--disable-gil" オプション付きでビルドかつ "PYTHON_GIL=0" 無し で実行 v3.13.0a5 & --disable-gil & PYTHON_GIL=0 : GILを無効化して実行 (単位: 秒) cocurrency v.3.12.2 v3.13.0a5 v3.13.0a5 & --disable-gil v3.13.0a5 & --disable-gil & PYTHON_GIL=0 1 1.12 1.01 1.47 1.46 2 2.29 2.07 3.00 1.56 3 3.46 3.13 4.56 1.74 4 4.62 4.17 6.02 1.78 5 5.74 5.22 7.37 1.98 6 6.91 6.35 8.82 2.06 7 8.08 7.40 10.29 2.23 8 9.22 8.42 11.84 2.40 9 10.38 9.47 13.26 2.56 10 11.53 10.52 14.80 2.71 GIL無効化( --disable-gil オプションでビルトかつ PYTHON_GIL=0 )の場合のみ所要時間が並列度に単純比例せず、期待した結果となりました。 また、GILが有効な v3.12.2 と v3.13.0a5 では単純に約10%程度高速になりました。 バージョンアップに伴って性能が改善されるのは嬉しいですね。 (6/14 訂正) 上記の3.12.2 と 3.13.0a5 の比較は正確なものではありませんでした。 筆者の環境で用意したランタイムが同じ条件でビルドされたものではありませんでした。 一方非マルチスレッド処理( concurrency=1 )においては性能が悪化しました。 上記の検証スクリプトの場合、データ構造の安全性に関するオーバーヘッドの影響が考えられます。 コレクション - python.jp PythonでGILの排除が難しい理由として、参照カウントの存在に加えて、Pythonインタープリタが辞書やリストなどの複雑なコレクションに依存している、という点もよく挙げられます。 残念ながら、手放しに喜べる検証結果とはなりませんでした。 今後のベータ版やrc版でも引き続き検証してみたいです。 (6/14追記) 追加検証: 3.13.0b2 2024/6/14 時点で最新バージョンである 3.13.0b2 を使って追加の検証を行いました。 また検証環境と手順を見直しました。 検証環境 専用環境として AWS Amazon EC2 インスタンス (仮想マシン) を用意しました。 リージョン: us-west-2 OS: Debian 12 (ami-0c2644caf041bb6de) インスタンスタイプ: c7a.2xlarge (8vCPU / 16GB) 手順 Docker Official Image のランタイムと同じ手順を再現しました。 python/3.13-rc/bookworm/Dockerfile at 748d6e9b44c0ee63e766a7c601d471e0763383d6 · docker-library/python 上記のベースイメージを辿っていくと必要パッケージのインストール、ビルド手順は以下の通りとなります。 検証手順 # 1. Prepare Build dependencies ## https://github.com/docker-library/buildpack-deps/blob/d0ecd4b7313e9bc6b00d9a4fe62ad5787bc197ae/debian/bookworm/curl/Dockerfile sudo apt-get install -y --no-install-recommends \ ca-certificates \ curl \ gnupg \ netbase \ sq \ wget ## https://github.com/docker-library/buildpack-deps/blob/d0ecd4b7313e9bc6b00d9a4fe62ad5787bc197ae/debian/bookworm/scm/Dockerfile sudo apt-get install -y --no-install-recommends \ git \ mercurial \ openssh-client \ subversion \ procps ## https://github.com/docker-library/buildpack-deps/blob/d0ecd4b7313e9bc6b00d9a4fe62ad5787bc197ae/debian/bookworm/Dockerfile sudo apt-get install -y --no-install-recommends \ autoconf \ automake \ bzip2 \ default-libmysqlclient-dev \ dpkg-dev \ file \ g++ \ gcc \ imagemagick \ libbz2-dev \ libc6-dev \ libcurl4-openssl-dev \ libdb-dev \ libevent-dev \ libffi-dev \ libgdbm-dev \ libglib2.0-dev \ libgmp-dev \ libjpeg-dev \ libkrb5-dev \ liblzma-dev \ libmagickcore-dev \ libmagickwand-dev \ libmaxminddb-dev \ libncurses5-dev \ libncursesw5-dev \ libpng-dev \ libpq-dev \ libreadline-dev \ libsqlite3-dev \ libssl-dev \ libtool \ libwebp-dev \ libxml2-dev \ libxslt-dev \ libyaml-dev \ make \ patch \ unzip \ xz-utils \ zlib1g-dev ## https://github.com/docker-library/python/blob/748d6e9b44c0ee63e766a7c601d471e0763383d6/3.13-rc/bookworm/Dockerfile sudo apt-get install -y --no-install-recommends \ libbluetooth-dev \ tk-dev \ uuid-dev # 2. Build / Install mkdir sandbox cd sandbox/ ## https://github.com/docker-library/python/blob/748d6e9b44c0ee63e766a7c601d471e0763383d6/3.13-rc/bookworm/Dockerfile#L22-L87 ## and `--disable-gil` export GPG_KEY =7169605F62C751356D054A26A821E680E5FA6305 && \ export PYTHON_VERSION = 3 . 13 .0b2 && \ wget -O python.tar.xz " https://www.python.org/ftp/python/ ${PYTHON_VERSION %% [a-z]* } /Python- $PYTHON_VERSION .tar.xz " && \ wget -O python.tar.xz.asc " https://www.python.org/ftp/python/ ${PYTHON_VERSION %% [a-z]* } /Python- $PYTHON_VERSION .tar.xz.asc " && \ GNUPGHOME = " $( mktemp -d ) " ; export GNUPGHOME && \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys " $GPG_KEY " && \ gpg --batch --verify python.tar.xz.asc python.tar.xz && \ gpgconf --kill all && \ rm -rf " $GNUPGHOME " python.tar.xz.asc && \ mkdir -p ./python && \ tar --extract --directory ./python --strip-components = 1 --file python.tar.xz && \ rm python.tar.xz && \ cd ./python && \ gnuArch = " $( dpkg-architecture --query DEB_BUILD_GNU_TYPE ) " && \ ./configure \ --build =" $gnuArch " \ --disable-gil \ --enable-loadable-sqlite-extensions \ --enable-optimizations \ --enable-option-checking = fatal \ --enable-shared \ --with-lto \ --with-system-expat \ --without-ensurepip \ && \ nproc = " $( nproc ) " && \ EXTRA_CFLAGS = " $( dpkg-buildflags --get CFLAGS ) " && \ LDFLAGS = " $( dpkg-buildflags --get LDFLAGS ) " && \ make -j " $nproc " \ " EXTRA_CFLAGS= ${EXTRA_CFLAGS :- } " \ " LDFLAGS= ${LDFLAGS :- } " \ " PROFILE_TASK= ${PROFILE_TASK :- } " \ && \ rm python && \ make -j " $nproc " \ " EXTRA_CFLAGS= ${EXTRA_CFLAGS :- } " \ " LDFLAGS= ${LDFLAGS :- -Wl } ,-rpath=' \$\$ ORIGIN/../lib' " \ " PROFILE_TASK= ${PROFILE_TASK :- } " \ python \ && \ make install && \ cd ../ && ./local/python3. 13 && ./local/python-3. 13 /bin/python3 -VV 結果 以下の4通りで比較しました。 Python 3.12.4 docker run python:3.12.4 python -c "$(< test_gil.py)" Python 3.13.0b2 docker run python:3.13.0b2 python -c "$(< test_gil.py)" Python 3.13.0b2 (GIL diabled): 環境変数 PYTHON_GIL=1 Python 3.13.0b2 (GIL diabled): 環境変数 PYTHON_GIL=0 リリース版である 3.12.4 および「GIL 有効化の 3.13.0b2」はDocker コンテナで実行しました (ランタイムの条件をなるべく揃えるため)。 また実行したスクリプト test_gil.py は前述のものとほぼ同じですが、最大並列度はコア数の2倍 (= 16) まで上げてみました。 cocurrency v.3.12.4 v3.13.0b2 v3.13.0b2 & --disable-gil & PYTHON_GIL=1 v3.13.0b2 & --disable-gil & PYTHON_GIL=0 1 0.96 0.88 1.16 1.13 2 1.55 1.72 2.17 1.13 3 2.33 2.57 3.23 1.13 4 3.10 3.43 4.27 1.14 5 3.87 4.28 5.31 1.14 6 4.72 5.14 6.36 1.15 7 5.45 6.01 7.41 1.16 8 6.26 6.87 8.44 1.19 9 7.06 7.73 9.52 1.47 10 7.90 8.58 10.65 1.87 11 8.61 9.43 11.65 1.84 12 9.43 10.29 12.67 1.93 13 10.28 11.13 13.85 1.93 14 11.16 11.99 14.90 2.13 15 11.97 12.86 15.91 2.22 16 12.68 13.70 17.13 2.36 概ね同様の結果になりました。 GIL 無効化の場合では CPU コア数 (= 8) を超えたあたりで所要時間のベースラインが上がっていることが分かります。 また、今回の結果では僅かですが 3.12.4 が 3.13.0b2 の結果を上回りました。 まとめ Python3.13の開発バージョンを用いてのGILの解消を確認しました。 プロセスあたりのマルチスレッド処理の性能向上が期待できますね。 GILの解消を提案したPEP 703によるとターゲットバージョンはPython3.13であり、順調に開発が進めば2024年10月にリリースされることになりそうです。 参考 PEP 703 – Making the Global Interpreter Lock Optional in CPython PEP 719 – Python 3.13 Release Schedule