TECH PLAY

株式会社ラクス

株式会社ラクス の技術ブログ

941

こんにちは!フロントエンド開発課の koki _matsuraです。 この記事では、僕が開発に携わっている製品のE2Eテストに取り入れたページオブジェクトモデル(POM)という実装パターンの概要と取り入れたキッカケ、POMへ リファクタリング する簡単な例をご紹介させていただきます。 僕と同じようにE2Eテストに関わっている方、E2Eテストに興味を持っている方などに読んでいただけると幸いです。 目次は下記のようになっています。 POMとは なぜPOMを使い始めたのか POMへのリファクタリング ログイン画面 テスト内容 POM導入前のテストコード ページオブジェクト作成 POM導入後のテストコード 終わりに POMとは Webアプリケーションのテスト自動化において、テストコードとWebページを分離して管理する手法です。 POMを使わない従来のテストコードはWebページと分離しないため、どうしてもDOMの構造を意識したものになってしまい、コードは長くなり読みにくくなります。大規模になると保守性なども問題になってきます。 下図はPOMを使わないテストです。同じ要素を複数回取得する必要があり、テストコードの重複もあります。 POMはこれらの問題点を解消します。具体的にはWebページごとのクラス(ページオブジェクト)を作成し、各クラス内でそのページの要素に対する操作をメソッドとして定義します。テストコード側はページオブジェクトを呼び出し、メソッドを使うだけで要素を操作できます。 下図はPOMを使ったテストです。テストコードとWebページは分離されています。テストコードはDOMを意識せずにメソッドと アサーション だけを使えば簡単にテストが書けます。 まとめるとPOMの利点は以下が考えられます。 可読性の向上 テストコードではDOMを意識しなくて済むため、簡潔なコードとなり読みやすくなると考えられます。 保守性の向上 DOMに変更があっても、ページオブジェクトを変更するだけでテストコードの変更は不要になるため、保守性は向上します。 再利用の向上 ページオブジェクトを作成しておけば、そのページの様々なテストケースにおいてコードの重複を防ぐことができます。 チーム開発の効率化 テストコードをチームメンバーで共有しやすくなります。また、テストコードを迅速に作成することができます。 なぜPOMを使い始めたのか 上記で利点をいくつか挙げましたが、その中でもPOMを使い始めた一番の理由は「保守性の向上」です。 POMを使う前は愚直に多くのページに対して テストコードを書いていました。 テストコードを書き続けるうちに1つ1つのページに対してのテストケースが多くなっていき、1つのケースに対して関係してくるページも多くなっていました。 そして、ある日、DOMを変更した瞬間、今まで問題なく通っていたE2Eテストは落ちてしまいました。1箇所の変更だけでもそのDOMが関わるテストは下図のように全て落ちてしまいます。 そこからは何か仕様変更が起こるたびに下記のようなループが起きます。 「仕様変更発生」→「DOMの変更」→「大量のテストが落ちる」→「大量のテストの修正」→「仕様変更発生」→ ...以下略 DOMの変更点が多い日には超大量のテスト修正という虚無の時間が訪れます。 このループが続いているとテストの失敗を放置してしまうようになってしまいます。 「せっかくE2Eテストを書いたのに...」「でも、毎回メンテナンスするなんて...」「もっと修正点が少なくなればいいのに...」とメンタル的にもしんどくなってきました。 同じような経験をした人や今現在している人もいるのではないでしょうか。 テストの失敗が続いてしまうと、信頼性も下がってきます。テストの意味もなくなってきます。もっと保守性の高いコードにするべきです。 そこでPlaywrightのドキュメントを読んでいたときに見つけたのが「ページオブジェクトモデル」です。 今までのWebページとテストコードの間にページオブジェクトを挟むことでページをオブジェクトとして扱える! DOMの変更が起きても、修正するのは該当のページオブジェクトでテストコードは修正しなくて済む! 操作をメソッド化すればテストコードが簡潔になり、テストコードに慣れていない人でも簡単に読める! ページオブジェクトを作る手間はかかるけど、あの虚無の日々を考えたら全然大したことない! 保守性の高いテストが書ける! と思ったため、導入するに至りました。 今では仕様変更が発生しても、下図のようにページオブジェクトのみを修正すればすぐに全てのテストが動くようになり、大幅に修正の時間も減り、保守性を高めることができました。 POMへの リファクタリング POMの概要とどのような経緯で使うことに至ったのかの説明をしてきました。 最後は実際に簡単なログイン画面をもとに普通のテストコードからどのようにPOMへ リファクタリング していくかを説明させていただきます。 ログイン画面 ログイン画面は以下のようにシンプルに「名前」と「パスワード」の入力欄があります。 テスト内容 ログイン画面へ遷移する 名前入力欄に「user」を入力 パスワード入力欄に「password」を入力 ログインボタンをクリック 一覧画面へ遷移できているかテスト POM導入前のテストコード 名前入力欄、パスワード入力欄の要素へ記入し、ログインボタン要素をクリックしています。 ログイン後、トップ画面へ遷移できているかはURLを見ることでチェックします。 ※ 下記のコードではパスワードをベタ書きしていますが、Gitなどに上げる場合にはenvファイルを経由するなどして直接は書き込まないようにしてください。 // login.spec.ts import { test , expect } from '@playwright/test' ; test ( "ログインできているか" , async ( { page } ) => { await page. goto( "/login" ); await page.getByLabel ( '名前' ) .fill ( "user" ) await page.getByLabel ( 'パスワード' ) .fill ( "password" ) await page.getByRole ( 'button' , { name: 'ログイン' } ) .click (); await expect ( page ) .toHaveURL ( "/" ) } ) ページオブジェクト作成 ページオブジェクトを作成するために適当な ディレクト リでpageObjectフォルダを作成します。 おすすめとしてはプロジェクト直下にE2Eフォルダを作成し、その中で E2E/tests/*.spec.ts や E2E/pageObject/*.ts などを管理するのが良いと思います。 では、ページオブジェクトを作成していきます。 基本は下記のような「要素の定義」「コンスト ラク タの定義」「操作の関数定義」の形で作成します。 goto関数やlogin関数はこのテストでは必須です。 waitForPageContentsのような読み込みを待つ関数は要素の読み込みを待たないことによるテストの失敗が多い場合に定義すると良いと思います。 // loginPage.ts import { expect , Locator , Page } from '@playwright/test' ; export class LoginPage { // 要素の定義 readonly page : Page readonly name : Locator readonly password : Locator readonly loginButton : Locator // コンストラクタの定義 constructor( page : Page ) { this .page = page this .name = page.getByLabel ( "名前" ) this .password = page.getByLabel ( "パスワード" ) this .loginButton = page.getByRole ( 'button' , { name: 'ログイン' } ) }   // 関数の定義 async goto() { await this .page. goto( "/login" ) } async waitForPageContents () { await this .name.waitFor () await this .password.waitFor () await this .loginButton.waitFor () } async login ( name: string , password: string ) { await this .name.fill ( name ) await this .password.fill ( password ) await this .loginButton.click () } } POM導入後のテストコード ログインページのページオブジェクトが完成したので、実際にテストコードを リファクタリング していきましょう。 やることは簡単で、テストコードの先頭で インスタンス を生成して、あとは操作を記述していくだけです。 今回の場合は下記のようなコードになります。 import { test , expect } from '@playwright/test' ; import { LoginPage } from '../pageObject/loginPage' ; test ( "ログインできているか" , async ( { page } ) => { const loginPage = new LoginPage ( page ); await loginPage. goto(); await loginPage.login ( "user" , "password" ); await expect ( page ) .toHaveURL ( "/" ); } ) ページオブジェクトを用いてログインのテストを リファクタリング ができました。 テストコードはDOMを意識したものではなくなっているので仕様変更が起きてもテストコードを変更する必要がありません。 終わりに 今回はE2Eテストにページオブジェクトモデルを導入した話をさせていただきました。 どうでしょうか。ページオブジェクトモデルの導入によりテストコードは多少見やすくなりましたが、色々実装することを考えると微妙だなと感じた人もいるのではないでしょうか。 僕も最初はそのように感じました。保守性を高めるメリットに対して、ページオブジェクト作成とテスト改修のコストがかかるというデメリットがあるため、微妙だと感じやすいです。 しかし、テストケースが増えたり、テストする画面が増えたりなど、大規模になればなる程、ページオブジェクトモデルは真価を発揮するものです。 なので、自分が開発している製品の規模感に合わせて導入するかを検討するのが良いと思います。 ここまで読んでいただきありがとうございます。この記事を機に、少しでもページオブジェクトモデルに興味を持っていただけたら幸いです。
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年7月のイベントでは「PHPDoc」について語り合いました。 弊社のメンバーが事前にまとめてきたPHPDocの情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com そもそもPHPDocとは何か 活用するポイント 課題 代表的な書き方 基本的なタグ 型の記述 PHPDocに関する質問 最低限書いておきたいコメント 必須 微妙 その他 phpDocumentor使ってますか? PHPDocの記載がすべて出力されるか phpDocumentorの使いどころは? PHP TechCafeメンバーの中で出た意見 phpDocumentorで出力されるもの WEB APIに関しての使いどころ PHPDocの記述だけで WEB APIの動作確認をする方法はないか APIドキュメントを書いておいて、フロントエンドとバックエンドを疎結合にする開発 戻り値がvoidの場合、”@return void”は書いたほうが良いか? @throws\Throwableは書くべき? 各IDEでサポートしているPHPDocの範囲が分からない レガシーシステムとPHPDocの向き合い方 他の言語のDocコメントとの違いは? まとめ 編集後記 そもそもPHPDocとは何か PHPDocについては、2021年5月に一度 【PHPDocについて語り合う】と題して、PHPTechCafeが開催されており、 その時のページを基におさらいをしました。 rakus.connpass.com PHPDocとは、関数、定数、クラス、メソッド、プロパティなどにブロックの説明として残すコメントのことです。 基本的にはただのコメントなのでプログラミングに影響はありませんが、一部のツールではPHPDocの内容によって処理を行うものもあります。 ただPHP8.0で アトリビュート が追加されたので、PHPDocとしての記述も アトリビュート 方式の記述へどんどん変わっていくのではないかというのが前回の見解でした。 活用するポイント 編集者の理解が捗る IDE で補完が効きやすくなる 静的解析がより正確に実行できる 実行時に型チェックされる 等が挙げられます 課題 明確な記述ルールは決まっていないというのが結構ネックです。 色々な書き方がありますが、 " PHP ドキュメントジェネレータであるphpDocumentorの記述が デファクトスタンダード なのか" とか、"PhpStormにあるようなものを採用すればいいのではないか" とか、色々な意見があります。 そのような点を標準化するべく、 PSR-5 や PSR-19 で議論が行われています。 今のところまだドラフト中ではありますが、PSR-5ではPHPDocのことが書いてあります。 コメントとして 「静的解析ツールでそれぞれ独自にやりたい方法でやっている感じがします。」 「PHPDocはPhpStormで生成されるものを書いているだけです」 というようなものをいただきました。 IDE で使えるものを使うという方もおられる印象です。 代表的な書き方 基本的なタグで、よく使うものをピックアップしています。 基本的なタグ @ から始まる文字列。何についてのドキュメントかを示す。 @param:関数またはメソッドの引数について記述 @return:返り値について記述 @throws:例外について記述 @var:変数について記述 @todo:開発でやるべきことがあることを示す 型の記述 前述の タグ の後に記述することで、対象の型を記述する 例 @return int 整数を返す リテラル 型 int, bool, string etc... 配列型 ただの配列(中身不明): array 数値キーの配列: int 複数の型があり得る配列: (int|string) 文字列キーの配列: array<string, int> キーごとに値の型が異なる配列: array{id: int, name: string} その他の書き方 false型 booleanではなくfalse型 property __get() , __set() を使った動的なプロパティを記述することが可能 ローカル変数の型指定 @var で指定 このようなことを記入しておくと、 IDE で補完が効いたり静的解析でチェックされるというメリットがあります。 基本的なものは上記のとおりですが、型の記述ではBoolean型ではなくfalse型があったり、 __get() , __set() を使った動的なプロパティを作れるpropertyなどがあります。 このfalse型などの変わったものについては まとめページ に書いてあるので、目を通していただければと思います。 この中でいただいたコメントでは、 「個人開発ではめっちゃ@todo使います。これを書いているとPhpStormとかでコミットする時に「まだTODO残ってるよ」って教えてくれますね。」 「@varは変数とかよりはpropertyのためのものだと思ってもらった方がいい。PHPStan対応で必要な場合を除いてあまり書かずに済ませたいですね。」 さらに興味深いところでは 「 アノテーション に進捗状況を記入して、 ガントチャート を自動生成する」 というものをいただきました。 これについては、 「その発想はなかった!」 「嫌だそれ。 アノテーション に プログレ スとか書いて、このクラスどこまで出来上がったみたいな、そこから自動的に ガントチャート に進捗率が反映されるって。中々ですね。」 「「@progress 70%」とか世知辛いですね~、どうせこれ人が書いているから99%とかで止まっているんでしょうね。」 というような、いかにも業界な感想で話が盛り上がりました。 PHPDocに関する質問 今回は ”そもそもPHPDocってどういう時に使うか” という点を念頭に議論が行われました。 最低限書いておきたいコメント 必須 @var @param @return @throws(明示的にthrowしているなら) @var,@param,@returnに関しては、ソース上型を書けば十分という意見もあります。@throwsは何かあった時にツールが補完してくれるということも考えられます。 「@returnや@paramはArrayの時だけかな」というコメントもいただきましたが、 これについては、(Arrayの)中身ということで考えれば、記載があった方が良いと思われます。 ただ、変なArrayになるのであれば、クラスで返す事などを考えて、Arrayを受け取らなければならないシーンを無くしていきたいと思います。 他にも 「型宣言と同じものをPHPDocに書き写さなくて良い」 「記載不要と思っていてもPhpStormがコメントを入れるよう求めてくる」 というコメントをいただきました。 こちらについては、型宣言しているのなら二重メンテになるので記載不要と思われますが、PhpStormは入れるよう警告してきますので「そこまで補完しなくてもいいのでは?」という意見もあがりました。 微妙 @use/@used-by @package @use/@used-byは、可変の関数を使ったときに、”ここで使っています” と、示すために使うものです。 ですが、@use/@used-by で説明しなければいけないようなロジック自体をできるだけ避ける方が望ましいと思われます。 @package は、 名前空間 の対応物または補足として使用できます。 要素を異なる階層でグループ化できる論理的な細分化を提供できます。 その他 「参考となる物が存在する場合は @link を使う場合があります」 というコメントをいただきました。 コードのコメントとしては、”意味があるものは書くべき” と思いますので、@linkの記述はあると便利です。 また、ここまでの話から、 「 PHP に ジェネリクス 導入してほしいしてほしい」 というコメントがありました。 そうなるとPHPDocのコメントを書かなくてよくなることも考えられそうですが、 半面、 「「言語組み込み ジェネリクス はいらない」という考え方もあり、実行時それまでやってしまうと重くなり、そこはPHPDocで収めておくのがいいんじゃないか 」 「パフォーマンスに影響するなら考えものですね。PHPDocで良い気がする」 というコメントもありました。 phpDocumentor使ってますか? phpDocumentor は PHP プロジェクトのドキュメントを自動で作成してくれるツール 基本的なPHPDocコメントだけでも作成してくれうのでかなり有効 CIに組み込めばドキュメントのメンテナンスコストも削減できると考えられる と紹介されました。 phpDocumentorを使うことによってプロジェクトのドキュメントを自動で作成してくれますので、 基本的にはPHPDocコメントが書かれていれば、人によっては見やすいドキュメントを生成します。 Documentation のサイトでは、出力されるドキュメントのイメージを確認できます。 右側にあるSearch検索ボックスにクラス名などを入力するとクラス一覧が表示されます。 それをクリックすると説明が表示されます。 この ページ のTable of Contentsに P と書いてあり鍵マークが外れているものがpublic、鍵が付いているものがprivate、 M がmethod、interfaceは I となっています。 PHPDocの記載がすべて出力されるか 主催者側で検証したところ、PHPDocで型を書いているところは全部出力してくれたようです。 プロダクトではまだ使っていませんが、試しに実行してみたところしっかり出力してくれたようです。 ドキュメントのメンテナンスコストが減り、CIに組み込めば開発コストも下げられる期待があります。 phpDocumentorの使いどころは? PhpStormを使っている場合、調べたいものがあればShift+Ctrlで調べられたりします。 詰まるところ ”みんながコード読める環境ならphpDocumentorによるドキュメントは無くても良い” ということなのでしょうか。 使いどころを考えてみます。 PHP TechCafeメンバーの中で出た意見 「受託開発で一部機能だけ作ってくれという案件で納品する時にあると良いのかな」 「 API のドキュメントであれば、内部実装よりは「何を受けて何を返すのか」が最低限分かればいいので、簡単にチェックできるものがあれば良いのかな」 という意見があったことが紹介されました。 参加者の方からは、 「ライブラリにはあってほしい」 というコメントをいただきました。 ライブラリ内部までは不要ですが、どう使ったら良いかはわかると嬉しいと思います。 「ライブラリも基本的にソースを読んじゃうから、別にドキュメントいらない説」 「英語を読むより ソースコード のほうが読みやすくないですか?」 そんなコメントもいただきました。 これについては PHP TechCafeのメンバーの中では、”PhpStormで検索すればだいたい賄えるのでは”とも話が挙がっていました。 phpDocumentorで出力されるもの PHPDocで型定義をしていない変数については、phpDocumentorは出力してくれるのでしょうか? この疑問については、以下の結果が紹介されました。 PHPDocに書いていなくてもコードで定義されている型が反映される 引数なども反映される PHPDocを書いていれば、関数の説明などのコメントも反映される WEB API に関しての使いどころ WEB API の仕様をコメントの中に記述していくことが考えられますが、ここからWEB API に関して話が盛り上がります。 Swagger などはその場で実行できるので便利ですが、 API のドキュメントとなると意識的に最新化し続けないといけないという意見が挙がりました。 WEB API がドキュメント化されていても、 PHP に慣れている人からすると ”ソース読めばよいのでは” という考えも出てきます。 とはいえ、ざっくりと概要を知りたい人にはドキュメントがあったほうが助かるかもしれません。 PHPDocの記述だけで WEB API の動作確認をする方法はないか PHPDocの記述の中に、 API の仕様を記載しておくだけで API の動作を試すようなことはできないのでしょうか。 実際にそのようなものが OSS に存在しているのでしょうか。 参加者の方が、見つけてくださいました。 swagger-php NelmioApiDocBundle API ドキュメントを書いておいて、フロントエンドとバックエンドを 疎結合 にする開発 先にOpenAPIを使って API 仕様を決めておけば、バックエンドとフロントエンドを分けて開発する事ができそうですが、 PHPDocの記載にによって API 仕様を定義するのであれば、先にバックエンド側のコードの方に API 仕様を書くことになるため、バックエンドとフロントエンドを同時に分けて開発することは難しいと思われます。 他にも以下のような意見が寄せられました。 Swaggerを使う場合、別環境を準備しなければいけない為、個人開発でミニマムに済ませたいときはPHPDocに書いたほうが楽なのではないか。 PHPDocはコメントとしての力が強いので、自由に書けるというのがメリット 戻り値がvoidの場合、”@return void”は書いたほうが良いか? 「書かないとPHPStanで怒られる。」 というコメントをいただきました。 「 わざわざそこまで書くのか?」 「言語の方で補えるものは書かなくても良くなってきているかもしれないです。」 という話もありましたが、 void を書くのが圧倒的多数のようでした。 @throws\Throwableは書くべき? これは「書きたい」という意見が多かったようです。 言語仕様のほうで引数の型や戻り値の型は表現できますが、@throws\Throwableの方はしっかり書いといたほうが良さそうです。 「PhpStormでWarningが出るときと出ない時がある。 」 「CatchしているときはThrowsを書かないと警告」 「逆に投げているのにCatchしていないとそれも警告」 というコメントもいただきました。 各 IDE でサポートしているPHPDocの範囲が分からない PhpStormの提言として、PHPDocにどこまで対応しているかというリストはないようです。 公式の記載 である程度情報がまとまっているようですが、特にどこまで対応しているという記載はありません。 PhpStormが公開しない理由があるのでしょうか。 「勢いよく増えていくから、書いているうちに古くなるから。」 「メンテナンスのコストがかかるんでしょうか。 」 「結局PSRの議論がクローズしないのも、こういうことがあるからなのでしょうか。 」 という意見がありました(実態は不明のようです)。 レガシーシステム とPHPDocの向き合い方 レガシーなシステムをメンテナンスする際のPHPDocの扱い方についての議論が行われました。 PHPDocはあくまでもコメントですので、型エラーになるわけでもありません。そのため、レガシーなシステムにもPHPDocを書いていくほうが良いのではないかという考えが紹介されました。 どうしてもレガシーなところでコードを直 接触 るのをためらうときには、PHPDocコメントを書いて解析に委ねるのも戦略の一つです。 レガシーな記述方法で言えば、その一つに、array shapes 1 がありますが、素直にコメントを書いていくとすれば大変です。 リファクタリング によりクラスに置き換えることできることを考えると、PHPDocの記述で補完するよりは リファクタリング することを優先したほうが良いように思います。 他の言語のDocコメントとの違いは? 参加者から以下のような意見がありました。 JavaDoc 「あまり詳しく無いんですがあまり規約とか無いんですかね。」 Python 「派閥的なものがあるみたいです。」 PHPDocも、前述の通り、 ”これは必ず書かないといけない” という決まったルールはまだありませんが、それは他の言語でも同様のようです。 とは言え、 ”これくらいは記載したいよね” という緩いルールのような共通認識はあるようです。 まとめ 「考えていることがみんな似ていてホッとする」 「PHPDocを深く考えたことがなかったので勉強になりました」 というコメントをいただきました。 明確なルールがない部分もありますが、参加者同士で疑問を共有して考えを知れたので良い機会となりました。 主催者も含めて、この機会に勉強することができたようです。 編集後記 以上、PHPerのための「PHPDoc相談会」として、PHPDocについて深く掘り下げていきました。 PHP で書くコメントは開発者が普段から当然のように利用しているものですので、主催者/参加者ともに非常に盛り上がりを見せていました。 今後も PHP に関することや、新たなニュースに着目していきたいと思います! 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 PHPDocの配列表記方法の一つ。キーごとに値の型が異なる配列等を表現する。 ↩
.entry-inner img{ border: 1px solid #000; } こんにちは!技術広報課の rks_daigo と申します。 コロナ禍では多くの企業でビアバッシュ等のオフラインイベントが制限されていたと思いますが、 弊社も感染拡大を防ぐためにオフラインでの開催を自粛しておりました。 そのような中、2023年に入り新規感染者が落ち着いてきたタイミングで、感染にも配慮しながらオフラインイベントを再開しておりました。 本記事では4月に開催しました『ChatGPやってみたビアバッシュ@東京』が大変盛り上がりましたので、 そちらの様子をご紹介したいと思います! ※大阪開発拠点でも毎月ビアバッシュを開催しておりますので、また別のレポートでご紹介できればと思います。 今回のビールのお供 3月に開催された デザインビアバッシュ に触発され、おしゃれなオードブルを用意してみました。 ビールのお供 みんな喜んでくれたかな?と事後アンケートで次回のリク エス トを聞いてみたところ ピザ ×4 ピザ・お寿司 ピザや寿司があると嬉しいです 前回のように寿司&ピザみたいなものだと嬉しいです 寿司、ピザが嫌いな人はいないと思います! 銀のさら どんだけピザと寿司好きやねん。 次回はピザと寿司にしたいと思います。。 LTテーマ:「ChatGPTやってみた」 旬過ぎるテーマですね。テーマのパワーもあり、多くの方に参加していただきました。 司会は新卒で2年目のだーやまさんです。上手にまわしていただきました。感謝。 会場の様子 LT発表内容 ChatGPTを見習ってChatBotを作ろう~一家に一台ChatBot~ Google Colab上でChatBotを実装した話。Dolly 2.0使ってたのがよかったです。 おまけで Stable Diffusion も Colab に実装してましたが、ヤツは時間が溶ける。 発表者:ぐっちさん(フロントエンド開発課) LLM活用事例を考える_AutoGPT,LlamaIndex 話題のAutoGPTの実装デモ見れたのがよかった! LlamaIndexでチャットボットがさくっと作れてしまう時代ですね。 発表者:だーやまさん(楽楽勤怠 開発1課) LLMのど素人がOpenAIのEmbeddingAPIを使ってChatBotを作ってみた 文字列からベクトルを取得してコサイン類似度で突合する話です。(なんそれ) スライドも自己紹介も自動生成するというAIづくしのLTでした。 発表者:えーいちさん(楽楽勤怠 開発1課) Steamレビューを解析して要約してくれるアプリ Gamer GPT を作ってみた ChatGPTのレスポンス待ちをゲームのローディング画面っぽく実装。よいア イデア です。 アプリのUIが素敵でした。Steamって API あるんですね。 発表者:とっしーさん(フロントエンド開発課) ChatGPT加熱しすぎ問題 ChatGPTの急激な盛り上がりに一石を投じるLT。ここでは何も書けないw 技術推進課では、AI関連もどんどん追っかけていくみたいです。イイネ! 発表者:いさむさん(技術推進課) ChatGPTを使っておかやま家の生産性をXX%改善した話 おかやま家のAI事情にほっこり。うちのおかんもこんな感じです。 シリーズ化を望みます。 発表者:おかやまさん(楽楽勤怠 開発2課) OpenAPIを利用した Kubernetes 診断ツールを触ってみよう(飛び込み) ビール飲みながらさくっと実装しビール片手に登壇ニキ。 さくっと実装しちゃうところが、かっこよかったですね。 発表者:MCさん(SRE課) 終わりに 今回は、弊社のビアバッシュをご紹介させていただきました! なんか楽しそうなことしてるなと思っていただけましたら幸いです。 ラク スの開発本部では、アウトプットすることを推奨しているため ビアバッシュの予算も毎月分、確保できており(感謝) エンジニアにとって「いつでも」「気軽に」LT登壇できる環境があるのは うれしいことなんじゃないかと、しみじみ感じております。 そんな ラク スでは、一緒にビアバッシュを盛り上げてくれる仲間を募集しております! 少しでも興味を持っていただけましたら、まずはカジュアルにお声掛けください。
はじめに こんにちは!フロントエンド開発課の koki _matsuraです。 この記事では、E2Eテスト フレームワーク として用いられるPlaywrightのインストールといくつか基本的なテストコード、最後に 拡張機能 についてもご紹介させていただきます。これからPlaywrightでテストを書きたい人、E2Eテストに少しでも興味を持っていただける方に読んでいただけると幸いです。 目次は次の通りになっています。 はじめに Playwrightとは インストール 簡単なテストを書いてみよう Playwrightの設定を編集しよう 別タブを開くテストを書こう 別タブをコードから開くケース 別タブをリンクから開くケース リクエスト・レスポンスをテストしよう モックを使ってテストしよう Playwright拡張機能を使おう テストの実行 実行ブラウザの変更 ヘッドレスモードの選択 要素の取得 テストコードの生成 おわりに Playwrightとは Webブラウザ の自動化テストを実行するためのライブラリであり、Node.jsアプリケーションで使用することができます。 スクレイピング やテスト自動化、UIテスト、パフォーマンステスト、 スクリーンショット 、PDFの生成などさまざまな用途で使用することができます。特徴は下記のようになっています。 Chrome ・ Firefox ・ Safari ・Edgeなど複数のブラウザをサポートしている 簡潔な API を提供しており、直感的にWebページを操作できる 非同期で動作することによる、テストの高速実行。ブラウザのクラッシュやフリーズに対する強い耐性を持っている Puppeteerと類似してることによりPuppeteerユーザーは取り入れやすい インストール Playwrightを使用するにはPlaywright Testのインストールとブラウザのインストールが必要となります。 下記のコマンドでPlaywright Testとブラウザを同時にインストールできます。 npm init playwright@latest インストールする際に次の項目について聞かれます。 TypeScriptか JavaScript のどちらを使用しますか (デフォルトはTypeScript) -> デフォルトを選択 テストフォルダーの名前 (デフォルトは tests または e2e) -> デフォルトを選択 GitHub Actions ワークフローを追加して、CI でテストをする -> falseを選択 Playwright ブラウザー をインストールします (デフォルトは true) -> デフォルトを選択 準備はこれだけです。あとはテストコードを作成するだけで簡単にテスト自動化を実現できます。 簡単なテストを書いてみよう 早速ですが、簡単なテストを書いてみましょう。テスト内容は次のようにします。 Chrome ブラウザで Google 検索エンジン のトップページを開く 検索項目に「株式会社 ラク ス」と入力し、Enterをクリック 「株式会社 ラク ス」の公式サイトのリンクをクリック ページが正しく表示されているかテスト テストのコードはデフォルトで作成されている tests/example.spec.ts に書いていきます。 例として書かれているテストを真似して、testの大枠を書いてみましょう。内容は一旦、コメントで書いておきます。 // example.spec.ts test ( 'ラクスのサイトが表示されているか' , async ( { page } ) => { // Google検索エンジンを開く // 検索欄に「株式会社ラクス」を入力 // Enterを押す // 検索結果から公式サイトをクリック // テスト:公式サイトが表示されているか } ) test関数は第一引数にテストの名前を、第二引数にテストの関数を書いていきます。テストの関数に引数として入っているpageとはブラウザで開かれたWebページを表すオブジェクトです。 テストを記述していきます。「 Google 検索エンジン を開く」の下に書きましょう。何かのページへ移動したい場合はgoto関数を使います。引数にはURLを入れます。 await page. goto( 'https://www.google.com/' ); これで Google 検索のページへ遷移するはずです。 次は、検索欄に「株式会社 ラク ス」と入力するコードを書いていきます。このコードには検索欄の要素を取得が必要です。ですが、要素を取得するのは意外と面倒です。 このような場合はPlaywrightのコードジェネレータを使いましょう。使い方は簡単です。下記のコマンドを打ちます。 npx playwright codegen https://www.google.com Playwright Inspectorと Chromium で Google 検索が開かれると思います。 Google 検索の検索欄の要素が欲しいので、そこにホバーします。すると、下記の画像のように要素が表示されます。 この要素をクリックして、検索欄に「株式会社 ラク ス」と入力し、enterキーを押します。すると、Inspectorの方に次のようなコードが生成されていると思います。 getByRoleとは名前の通り、要素を役割別に取得できるものです。今回はcomboboxというRoleを指定していますが、buttonやinputなどもあります。 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); 最後に公式サイトを検索結果からクリックするだけです。Inspectorから生成されたコードが次のようになっていれば問題ありません。 await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); 生成されたコードを example.spec.ts にコピペします。 // example.spec.ts test ( 'ラクスのサイトが表示されているか' , async ( { page } ) => { // Google検索エンジンを開く await page. goto( 'https://www.google.com/' ); // 検索欄に「株式会社ラクス」を入力 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); // Enterを押す await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); // 検索結果から公式サイトをクリック await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); // テスト:公式サイトが表示されているか } ) 最後に公式サイトが表示されているかをテストします。言い換えると、現在のページのURLが ラク スの公式サイトURLと一致するかのテストです。 次のコードを「テスト:公式サイトが表示されているか」のコメント下に追記します。 expectは アサーション ライブラリの一つで期待される結果を検証するときに用いるものです。pageが https://www.rakus.co.jp/ を持っているかの検証を示しています。 await expect ( page ) .toHaveURL ( "https://www.rakus.co.jp/" ); テストを書き終えたので実行をします。実行は下記のコマンドです。"9 passed"と表示されていればテスト通過できています。 npx playwright test レポートは npx playwright show-report を実行し、該当のページを表示すると見ることができます。 テストを実行することができました。 ちなみに、テストコードを書くためには対象となる要素を取得することが基本となり、PlaywrightではLocatorというWebページの要素を検索するためのオブジェクトを使います。 CSS セレクタ 、テキスト、要素の属性など、様々な方法で要素を検索できます。 とても便利ですが、Locatorはその便利さがゆえに使いすぎてコードがわかりにくくなったり、様々な検索が可能なために複雑なHTML構造を持つ要素を見つけるのが困難な場合があります。 なので、できる限りgetByRoleなどの明示的な組み込みロケーターを使います。組み込みロケーターはLocatorと比べ 、以下のようなメリットがあります。 シンプルでわかりやすいコードが書ける ボタンを特定するためにpage.locator()を使用するよりも、page.getByRole('button')のようにgetByRoleメソッドを使用した方がわかりやすいコードになります。 アクセシビリティ の改善 getByRole()などの組み込みのロケーターは、 Webアクセシビリティ の向上を目的としています。これらのロケーターを使用することで、 視覚障害 者やその他の障害を持つユーザーがWebページを使用するのに役立ちます。 ロケーターの選択が自動化される getByRole()などの組み込みのロケーターを使用することで、Playwrightが適切なロケーターを自動的に選択してくれます。getByRole('button')が実行されると、Playwrightは自動的にrole="button"属性を持つ要素を検索します。 Playwrightの設定を編集しよう テスト実行後のレポートを見てもらうと Chromium 、 Firefox 、 Webkit の3ブラウザで各テストが実行されていることが確認できます。もちろん複数ブラウザでテストすることも大事ですが毎回は時間がかかるのでブラウザを変更したい場合もあるでしょう。また、人によっては実行中のブラウザが表示して欲しい人もいると思います。 そのようなテストに関する設定は playwright.config.ts からできます。デフォルトでは下記のような設定になっています。 export default defineConfig ( { testDir: './tests' , /* Run tests in files in parallel */ fullyParallel: true , /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !! process .env.CI , /* Retry on CI only */ retries: process .env.CI ? 2 : 0 , /* Opt out of parallel tests on CI. */ workers: process .env.CI ? 1 : undefined , /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html' , /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' , } , /* Configure projects for major browsers */ projects: [ { name: 'chromium' , use: { ...devices [ 'Desktop Chrome' ] } , } , { name: 'firefox' , use: { ...devices [ 'Desktop Firefox' ] } , } , { name: 'webkit' , use: { ...devices [ 'Desktop Safari' ] } , } , ] } ); testDirはテストファイルを格納しておくフォルダのことで、ここに設定された ディレクト リ以外でテストファイルを作成してもPlaywrightは認識してくれないので注意です。 他はデフォルトでコメントをつけてくれているので省略します。 重要なのはprojectsで、デフォルトでは chromium 、 firefox 、 webkit が設定されています。 chromium だけで実行したい場合は、 firefox と webkit を コメントアウト します。これだけで実行ブラウザを変更することができます。 実行中にブラウザを表示するのも簡単です。reporterとprojectsの間にあるuseの中に下記を書き加えるだけです。 headless : false headlessオプションを書かない場合はデフォルトでtrueになっているので、falseを書き加えることで実行中にブラウザ表示させることができます。 保存して、もう一度テストを実行してみましょう。 すると、 chromium が3つ立ち上がり、テストがブラウザで実行されているのが確認できると思います。3つ立ち上がっているのはfullyParallelオプションがtrueになって、並列処理がされているためです。見にくい場合はfalseにすることで一つずつ実行されます。 オプションは他にも様々ありますので、詳しくは TestConfig | Playwright を見てください。 別タブを開くテストを書こう 別タブ操作機能の有無はE2Eテスト自動化ライブラリの選定基準の一つになっていることが多いと思います。Playwrightは別タブでの操作が可能となっています。 別タブを開くケースとしては「コードから自分で開くケース」と「ボタンを押すなどのトリガーにより開かれるケース」があると思われるのでその2パターンを紹介します。 別タブをコードから開くケース テストは内容は次のようにします。 ページを開く 別タブを開く 別タブで Google 検索エンジン のトップページを開く 検索項目に「株式会社 ラク ス」と入力し、Enterをクリック 「株式会社 ラク ス」の公式サイトのリンクをクリック 別タブのページが正しく表示されているかテスト 最初に書いたテストを別タブで行うだけです。テストのコードは example.spec.ts に書いていきます。 例として書かれていた二つは消してしまいましょう。 ページを生成するのは難しくありません。下記のようにBrowser型を使うだけです。 const page = await browser.newPage () これだけなので、テストも下記のように簡単に書けます。 test ( '別タブでラクスのサイトが表示されているか' , async ( { browser } ) => { // 最初のタブを開く const page = await browser.newPage () // 別タブを開く const newPage = await browser.newPage (); // 別タブでGoogle検索エンジンを開く await newPage. goto( "https://www.google.com" ); // 検索欄に「株式会社ラクス」を入力 await newPage.getByRole ( 'combobox' , { name: '検索' } ) .click (); await newPage.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); // 検索ボタンを押す await newPage.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); // 検索結果から公式サイトをクリック await newPage.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); // テスト:公式サイトが表示されているか await expect ( newPage ) .toHaveURL ( "https://www.rakus.co.jp/" ); } ) 実行してみると、別のタブが開くことが確認できると思います。 別タブをリンクから開くケース ラク スの公式サイトから別タブを開くリンクを押すテストにしました。内容は以下のようにします。 ラク スの公式サイトを開く ヘッダーメニューの「事業内容」をホバーする 表示される様々なリンクから「楽楽精算」をクリックする(自動で別タブで楽楽精算のページが開かれる) 別タブのURLが楽楽精算のページと一致するかをテスト example.spec.ts に書いていきましょう。 公式サイトを開くのはgoto関数で簡単にできます。 続いて、ヘッダーメニューとはサイト上部の「 ラク スの思い」「会社情報」などが並んでいる部分です。 事業内容のリンク要素を取得したいです。コードジェネレータを使えば簡単に取得できますが、下記画像のようにだいぶ長いコードになってしまいます。 コードジェネレータに頼りすぎるとわかりにくいコードになることもあるのであくまでサポートくらいにするといいと思います。 コードジェネレータでうまく要素が取り出せない時には、HTMLを確認してみましょう。IDやClassが割り振られていればかなり要素を絞れると思います。 ヘッダの各要素には p-header__menu__item というClassが割り振られていました。ClassやIDで取得する場合にはlocatorを使います。 あとは「事業内容」というテキストを取ればいいので次のようなコードで要素を取得できます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) かなりシンプルに取得できます。他にも下記のような取得方法もあります。この場合は「事業内容」というリンクがいくつかあり、その最初の要素が欲しいのでfirst()をつけることで取得できます。 await page.getByRole ( "link" , { name: "事業内容" } ) .first () 要素が取得できたのでホバーをするだけです。ホバーは要素にhover関数をつけるだけなので下記のように簡単に実装できます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .hover () 別タブでページを開く際にはイベントが発生する前にイベントを待つ関数を定義しないといけません。これは下記のように書けば実装できます。 const newPagePromise = page.waitForEvent ( 'popup' ); イベントを発生させましょう。「楽楽精算」というリンクをクリックするコードです。ここは下記のようにgetByRoleでリンク要素かつ楽楽精算というテキストで要素を絞ります。exactとは完全一致を示します。exactをつけないと部分一致になってしまいます。 await page.getByRole ( 'link' , { name: '楽楽精算' , exact: true } ) .click () 別タブで「楽楽精算」のページが開くはずなので、イベントを解決しましょう。先ほどのnewPagePromiseはPromise 型になっているのでawaitをつけることでPageが取り出せます。 const rakurakuseisanPage = await newPagePromise rakurakuseisanPageは「楽楽精算」のページを表しているはずです。 最後、rakurakuseisanPageが「楽楽精算」のページURLと一致するかのテストを書きます。ここは他のテストと同じように書けます。 await expect ( rakurakuseisanPage ) .toHaveURL ( /^https:\/\/www\.rakurakuseisan\.jp\/.*$/ ); これでテストが書けました。念の為、テストコードを全て載せておきます。 test ( '別タブで楽楽精算のページが開かれているか' , async ( { page } ) => { // 最初のタブでラクスの公式サイトを開く await page. goto( 'https://www.rakus.co.jp/' ); // ヘッダーのリンクをホバーする await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .hover (); // 新しいページを開くイベントを待つ const newPagePromise = page.waitForEvent ( 'popup' ); // 楽楽精算のリンクをクリックする(新しいページを開くイベント) await page.getByRole ( 'link' , { name: '楽楽精算' , exact: true } ) .click (); // イベントを解決する const rakurakuseisanPage = await newPagePromise ; // テスト:新しいページのURLが楽楽精算のページと一致するか await expect ( rakurakuseisanPage ) .toHaveURL ( /^https: \ / \ /www \ .rakurakuseisan \ .jp \ /.*$/ [ f:id:koki_matsura:20230505222421p:plain ] ); } ) テストを実行してみましょう。自動で開く別タブにも対応できることが確認できます。 リク エス ト・レスポンスをテストしよう 正常にリク エス トが飛んでいるか、レスポンスが想定通り返ってきているかを確かめたい時もあると思います。 そのようなテストもできるので簡単な例とともに紹介させていただきます。テスト内容は次のようになっています。 Google 検索エンジン のトップページを開く 検索項目に「Playwright」と入力し、Enterをクリック リク エス トとレスポンスを取得 リク エス トのparamに「Playwright」が含まれているか レスポンスのステータスが200で返ってきているか リク エス トとレスポンスを取得するのは新しいページを取得するのと同じでwaitoFor系の関数を使います。 具体的には次のようなコードで書くことができます。 const [ request , response ] = await Promise . all( [ page.waitForRequest ( request => request.url () .includes ( 'search?q=Playwright' )), page.waitForResponse ( response => response.url () .includes ( 'search?q=Playwright' )), // リクエストを発生させるイベント page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ) ] ) waitForRequestとwaitForResponse内の条件に当てはまった場合だけrequestとresponseが返ってきます。 なので、この時点でparamに「Playwright」が含まれていないとテストは失敗します。 リク エス トとレスポンスが取得できれば、あとは アサーション するだけです。 テストするのはリク エス トのparamに「Playwright」が含まれているかどうかとレスポンスのステータスが200で返ってきているかどうかです。 取得したリク エス トとレスポンスを使えば簡単に次のように実装できます。 // リクエストのParamに「Playwright」が含まれているか expect ( request.url ()) .toContain ( 'search?q=Playwright' ); // レスポンスのステータスが200かどうか expect ( response. status()) .toBe ( 200 ); リク エス トに関しては先ほどのwaitForRequestでparamをチェックしているのでもう一度確認する必要はないのですが一応、書いておきました。 残りの部分は他と同じなのでテストコードは次のようになります。 test ( '「Playwright」と検索されているか' , async ( { page } ) => { // Google検索エンジンを開く await page. goto( 'https://www.google.com/' ); // 検索欄に「Playwright」を入力 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( 'Playwright' ); // リクエスト・レスポンスを取得 const [ request , response ] = await Promise . all( [ page.waitForRequest ( request => request.url () .includes ( 'search?q=Playwright' )), page.waitForResponse ( response => response.url () .includes ( 'search?q=Playwright' )), // 検索を実行 page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ) ] ) // リクエストのParamに「Playwright」が含まれているか expect ( request.url ()) .toContain ( 'search?q=Playwright' ); // レスポンスのステータスが200かどうか expect ( response. status()) .toBe ( 200 ); } ) テストを実行してみましょう。正常に検索されれば、テストは通過するはずです。 モックを使ってテストしよう モックとは、リク エス トやレスポンスをテストする際に、実際のサーバーに接続せずに、事前に準備した偽のレスポンスを返すようにする機能です。 これを使用することで以下のようなメリットがあります。 テストの信頼性向上 外部 API はテスト実行中に変更されることがあります。これにより、実行結果が変化する可能性があります。モックを使用すると、毎回同じレスポンスを返すことができ、テストの信頼性を向上させることができます。 テストの速度向上 外部 API を呼び出すと、実行に時間がかかる場合があり、 タイムアウト になる可能性もあります。モックを使用すると、 API を待つ必要がないのでテストの速度を向上させることができます。 外部との依存排除 モックを使うと外部 API への依存関係を排除することができ、テスト実行環境の制約を減らすことができます。 エラーの発見と修正の易化 例えば、外部 API がレスポンスを返さない場合、モックを使用すればその API の問題を発見することが容易にできます。 モックはE2Eテストとは切っても切り離せない機能です。ぜひ、積極的に使っていきましょう。 早速ですが、テストの内容は次のようにします。 ラク スのページを開く ラク スのページ以下( https://www.rakus.co.jp/** )をモックする 「 ラク スの思い」(/about)の場合だけレスポンスを404にする 「 ラク スの思い」をクリックする 404ページが表示されているかをテスト Playwrightでモックを実装するにはroute関数を使用します。第一引数にはモックを反映するURLを、第二引数にはハンドラーを設定します。 このテストの場合は ラク スのページ以下を全てモックするので、下記のようにします。 await page.route ( 'https://www.rakus.co.jp/**' , rakusRouteHandler ); rakusRouteHandlerではURLが /about を含む時だけレスポンスを404にするという設定にしたいので、次のように実装しました。 const rakusRouteHandler = ( route: Route , request: Request ) => { const url = request.url (); if ( url.includes ( "about" )) { // レスポンスの内容 route.fulfill ( { status : 404 , } ); } else { // about以外は通常のレスポンスを返す route. continue(); } } ; requestのURLにaboutが含まれているときだけ404を返し、それ以外はroute.continue()で通常のレスポンスを返すようにしています。 これだけで簡単にモックすることができます。 テストの全コードは以下のようになります。 test ( "ラクスのページをモックする" , async ( { page } ) => { // モックするためのルートハンドラを作成 const rakusRouteHandler = ( route: Route , request: Request ) => { const url = request.url (); if ( url.includes ( "about" )) { // レスポンスの内容 route.fulfill ( { status : 404 , } ); } else { // about以外は通常のレスポンスを返す route. continue(); } } ; // ラクスのトップページを開く await page. goto( 'https://www.rakus.co.jp/' ); // トップページ以下のリクエストをモックする await page.route ( 'https://www.rakus.co.jp/**' , rakusRouteHandler ); // 「ラクスの思い」をクリック await page.locator ( '.p-header__menu__item' ) .getByText ( "ラクスの思い" ) .first () .click (); // テスト:404ページが表示されていること await expect ( page.locator ( 'h1' )) .toHaveText ( "この www.rakus.co.jp ページが見つかりません" ) } ) アサーション の部分はページのh1に「この www.rakus.co.jp ページが見つかりません」が表示されているかどうかを見ています。 テストを実行してみると、以下のように404ページが表示されていると思います。 また、 /about 以外は通常のレスポンスが返されるかも確認してみましょう。 「 ラク スの思い」をクリックする部分のコードを以下のように変えます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .click () テストを実行してみると、正常に事業内容のページが表示されていると思います。 ハンドラで実装した通りにできています。 今回は簡単に404を返す実装にしましたが、route.fulfill内を変えるだけで柔軟に様々なレスポンスを実装することができます。 Playwright 拡張機能 を使おう VSCode 使っている人専用にはなりますが、Playwrightの 拡張機能 がとても便利なのでご紹介します。 まずは VSCode の 拡張機能 から「Playwright」と検索をし、おそらく一番上に表示される「Playwright Test for VSCode 」をインストールしましょう。 インストールが完了するとエディターのサイドバーに三角ビーカーのアイコンが表示されていると思います。 テストの実行 サイドバーの三角ビーカーをクリックすると、 tests/ 以下のテストが表示されていると思います。 再生ボタンを押すことで実行することが可能です。テストファイルの方にも同様に各テストの先頭行に再生ボタンが表示されます。 成功したテストは緑のチェックマーク、失敗したテストは赤の バツ マークがつきます。 実行ブラウザの変更 拡張機能 を入れる前は playwright.config.ts のprojectsを書き加えたり、 コメントアウト したりして実行するブラウザを変えていました。 拡張機能 の場合は三角ビーカーをクリック、下画像の「Select Configuration」をクリック、「既定のプロファイルの選択」をクリックする。 下画像のようなセレクトボックスが表示されるのでここで実行したいプロジェクトを選択して、OKを押せば簡単に実行ブラウザを変えることが可能です。 ヘッドレスモードの選択 拡張機能 を入れる前はheadressをfalseにすることで実行中の画面を表示していましたが、テストの中にはわざわざ画面で見る必要のないものもあると思います。そのようなテストを実行するたびにheadressモードにしたりするのはとても面倒です。 拡張機能 の左下、「Show browser」という チェックボックス があります。 ここにチェックを入れるとheadressをfalseにしている状態と同じになります。チェックを外すとheadressモードになります。 playwright.config.ts に書いたheadressの設定は消さないとチェックしてもしなくても画面が表示されてしまうので注意です。 要素の取得 「Show browser」の下にある「Pick locator」をクリックするとブラウザが立ち上がります。そのブラウザでテストしたい画面に移り、欲しい要素をクリックすると、下画像のように VSCode 上部に要素のコードを記してくれます。 簡単に要素を取得したい分にはこれだけで十分です。 テストコードの生成 「Pick locator」の下にある「Record new」をクリックするとブラウザが立ち上がります。 ブラウザが立ち上がるとともに、 VSCode 上で新たなテストファイルも作成され、テストの雛形が書かれていると思います。 立ち上がったブラウザ上で操作をすると、全てこのファイル内にコードが生成されていきます。コードジェネレータと同じです。コマンドでジェネレータを立ち上げなくて済むのでかなり楽です。 テストの大枠をジェネレータに生成してもらい、細かい部分を自分で修正したりすることでかなり効率化できると思います。 既存のテストファイルにコードを生成して欲しい方は「Record at cursor」をクリックしてください。 コードを生成して欲しいファイルの適当な行を選択しておきましょう。 立ち上がったブラウザで操作すると、選択した行にコードが書き加えられていきます。 例えば、「簡単なテストを書こう」のテストと同じ操作をすると下記のようなコードが example.spec.ts に生成されました。 await page. goto( 'https://www.google.com/' ); await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); 検索欄への文字入力などが認識されていませんが、そこは自分で付け足せば、簡単にテストが作れます。 とても便利なのでどんどん使ってください。 おわりに 今回はPlaywrightを初めて書く人やE2Eに興味を持っている方に向けて、簡単なテストの例と 拡張機能 についてご紹介させていただきました。 Playwrightは直感的にテストを書くことができるので初めての人でもすぐに飛びつくことができ、コードジェネレータと組み合わせることで高速にテストコードを生み出すことも可能です。 まだまだ紹介していない機能がありますので、 公式サイト を見ていただけばなと思います。 とても長い記事になりましたが、ここまで読んでくださった方、ありがとうございます。 これを機にPlaywrightを使っていただければ、とても嬉しいです。
こんにちは、新卒2年目になりました菊池(akikuchi_rks)です。 新卒1年目では開発エンジニアとして様々な経験をさせていただきましたが、その1つとしてLaravel8→9へのバージョンアップ作業を行いました。 今回はこのLaravel9へのバージョンアップにおいて自分が躓いた経験から、注意が必要だと感じた点を紹介させていただきます。 はじめに 注意すべきこと 依存パッケージの確認 Trusted Proxiesファイルの修正 app/Http/Middleware/TrustProxies.php の修正 $headersの修正 Storageファサードを利用したファイル操作の仕様変更 ファイル書き込み時の仕様 略語を使用したメソッド名の変更 Unvalidated Array Keysの仕様変更 終わりに はじめに Laravel9が2022年2月8日、正式リリースされました。 標準 メーラー の変更、Flysystem3.xへのバージョンアップなど様々な変更点がありますが、これらの変更点に関する説明は他の記事で既にまとめられているため本記事では割愛させていただきます。 弊社でも毎月開催している勉強会「 PHP TechCafe」において『PHPerのための「Laravel9について語る」』というテーマで議論した回があり、 その内容のレポート記事で、Laravel9の変更点について簡潔にまとめられているので、良ければ参考にしていただければと思います。 tech-blog.rakus.co.jp Laravelのバージョンアップを行う際は、基本的に Laravel公式のアップグレードガイド でアップグレード手順を確認しながら作業を行うことになると思います。 手動で変更する必要がある修正内容については、ほとんどこちらの公式ドキュメントで説明されているのでこちらを参考に修正を行えば問題ありませんが、私が実際にバージョンアップ作業を行う中で 公式ドキュメントに書かれておらず修正が漏れていた 公式ドキュメントの説明を正確に理解出来ていなかった など、いくつか躓いた点がありました。 本記事ではこのような躓いた経験をもとに、注意すべきだと感じた点を紹介させていただきます。 注意すべきこと 依存パッケージの確認 バージョンアップ作業に入る前の話になりますが、アプリケーションの依存パッケージを事前に確認する必要があります。 特に、Laravel9ではSymfony6.x系に依存していますがSymfony6.x系をまだサポートしていないcomposerパッケージが意外とあります。 このような場合は、 別のパッケージを使用する パッケージを自力でカスタムする などの対応が必要になります。 Trusted Proxiesファイルの修正 Laravel9へのバージョンアップ対応において、Composerの依存パッケージの変更以外で手作業での修正が必須となるのはこちらの項目くらいだと思います。 そのため、Laravel9へバージョンアップ後にアプリケーションが動かなくなったという場合はまずはこちらの修正が正しく行えているかを確認すると良いと思います。 app/Http/Middleware/TrustProxies. php の修正 変更前 use Fideveloper\Proxy\TrustProxies as Middleware 変更後 use Illuminate\Http\Middleware\TrustProxies as Middleware $headersの修正 変更前 protected $headers = Request::HEADER_X_FORWARDED_ALL; 変更後 protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_POST | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB ; 因みに TrustProxies.php 以外のファイルでも HEADER_X_FORWARDED_ALL の定数が使われている場合は修正が必要です。 私の作業を行ったプロジェクトでも他のファイルで HEADER_X_FORWARDED_ALL が使われていましたが、そのことに気づかず修正が漏れていたことが原因でサービスが動きませんでした。 Storage ファサード を利用したファイル操作の仕様変更 Laravel9ではFlysystem 1.x から 3.xへの移行を行っています。 それに伴い、Storage ファサード を利用したファイル操作の仕様が一部変わっています。 ※参考:Flysystem公式ドキュメント flysystem.thephpleague.com ファイル書き込み時の仕様 特に注意すべきなのは、 write() 、 writeStream() でのファイルの書き込み時の仕様変更です。 Flysystemの公式ドキュメントにも書かれているように、以下の表のように書き込み関連のメソッドの仕様が変更されました。 Flysystemの書き込みメソッドの仕様変更表 Flysystem 1.xでは書き込み用のメソッドと更新用のメソッドが別で用意されており、 write() が上書き処理をすることはありませんでした。 また、書き込みと更新を自動で使い分けてくれるメソッドとして put() が用意されていました。 Flysystem 3.xでは以前の put() の機能を write() が担うようになり、 wirte() と update() は削除されました。 この仕様変更に伴い、Laravel9のStorage ファサード でput()やwrite()を使用した際は、どちらもFlysystemの write() が呼び出され、自動的に既存ファイルを上書きするようになりました。 もし、既存のファイルを上書きしたくない場合は、手動でファイルの存在確認をする必要があります。 この対応を怠ると、意図せず既存ファイルを上書きしてしまう、という事象が発生してしまうため注意が必要です。 略語を使用したメソッド名の変更 こちらはLaravel公式のアップグレー ドガ イドに載っていない内容ですが、該当のメソッドを使用している場合は対応が必要です。 Flysystem 1.x から 3.xへの移行の影響で省略された単語を含むメソッド名が省略なしの単語を使ったメソッド名に変更されました。 Laravelでは Illuminate\Filesystem\FilesystemAdapter.php に定義されていないメソッドをStorage ファサード で呼び出した際、Flysystemのメソッドを直接呼び出す仕組みがありますが、 この仕組みを利用して、今回変更があったメソッドを直接呼び出している場合は、変更後のメソッド名に修正する必要があります。 以下が変更されたメソッド名の例です。 変更前 $filesystem- > createDir($path); $filesystem- > deleteDir($path); 変更後 $filesystem->createDirectory($path); $filesystem->deleteDirectory($path); Unvalidated Array Keysの仕様変更 配列に対してバリデーションを行った際の挙動が変わりました。 具体的にはLaravelのバリデータが返すバリデーション済みデータにバリデーションしていない配列キーを含むかどうかが変わっています。 このあたりの仕様変更については、ドキュメントを読んだだけではいまいちピンと来なかったため、具体例を用いて説明しようと思います。 例えば 'staff' => ['name' => '田中太郎', 'age' => 26] のような配列のバリデーションをする場合を考えてみます。 <?php // ①バリデーションしていない配列キーを含む設定がされている場合 $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , 'staff.name' => [ 'string' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(2) { // ["name"]=> // string(12) "田中太郎" // ["age"]=> // int(23) // } //} // ②バリデーションしていない配列キーを除外する設定がされている場合 $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , 'staff.name' => [ 'string' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(1) { // ["name"]=> // string(12) "田中太郎" // } //} 上記の例のように配列の特定のキーに対してバリデーションを行わなかった場合に、 validate() で返されるデータがそのキーを含むかどうかに違いがあります。 以前のリリースのLaravelではデフォルトで①、アプリケーションのサービスプロバイダの boot() 内で excludeUnvalidatedArrayKeys() を呼び出している場合は②の挙動となる仕様でした。 一方で、Laravel9ではデフォルトで②、アプリケーションのサービスプロバイダの boot() 内で includeUnvalidatedArrayKeys() を呼び出している場合は①の挙動となる仕様に変更されています。 そのため、推奨はされていませんが、以前のリリースの動作を保つためには includeUnvalidatedArrayKeys() を呼び出す必要があります。 因みに、配列の要素に対してバリデーションを全く設定していない場合は②のようなバリデーションしていない配列キーを除外する設定にしていても、 validate() で返されるデータに全てのキーが含まれる挙動になっているようなので注意が必要です。 <?php $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(2) { // ["name"]=> // string(12) "田中太郎" // ["age"]=> // int(23) // } //} 終わりに 今回は、Laravel8からLaravel9へバージョンアップ作業を行う際の注意点について紹介させていただきました。 Laravel9は残念ながらLTSではなくなったので、Laravel10へのバージョンアップ作業も早々に取り組む必要がありそうですね、、 Laravel10へのバージョンアップを行う際は、今回の経験を生かしてよりスムーズに作業を終えられるよう頑張りたいです。 以上です。 本記事が少しでもLaravelバージョンアップ時の手助けになれば嬉しいです。
こんにちは。配配メール開発課のmoryosukeです。 最新のLaravelではデフォルトのフロントエンドビルドツールがLaravel MixからLaravel Viteへと移行しました。 そこでTailwind CSS をビルドする手順を追いながらLaravel Viteに慣れていこうと思います。 Laravel Viteとは 事前準備 プロジェクト作成 Tailwind CSSを導入する 補足 最後に Laravel Viteとは Laravel ViteはLaravel用の高速なフロントエンド・ビルドツールであり、 JavaScript のパッケージマネージャである Vite を使用しています。Vite は、フロントエンドのアプリケーションを高速に開発するためのツールであり、Webpack よりも高速で、リアルタイムのホットリロードや開発時の最適化機能を提供しています。 Laravelのバージョンが9.18以前はLaravel Mixがデフォルトで使用されていました。Laravel ViteとLaravel Mixの性能比較は以下の資料が参考になります。 speakerdeck.com readouble.com 事前準備 プロジェクト作成 今回はLaravel Sailを使ってプロジェクトを作成していきます。 Docker Desktopをインストール後、プロジェクトを作成する ディレクト リ下で以下のコマンドを実行します。 curl -s https://laravel.build/vite-sample | bash プロジェクト作成が完了するとvite.config.jsという設定ファイルが追加されていることが確認できます。 ここで以下のような設定が可能です。 root:プロジェクトのルート ディレクト リを指定する。 base:公開されるアセットのベースパスを指定する。 build:ビルドに関する設定を指定する。target、minify、outDirなどのプロパティがあります。 plugins:Vite プラグイン を追加するための設定です。例えば、PWA プラグイン やSass プラグイン などがあります。 server:開発サーバーの設定を指定する。port、host、proxy、 https などのプロパティがあります。 resolve:importやrequireで解決するファイルの拡張子などの設定を指定する。 ここでは、開発サーバの設定だけを追加で行います。 vite.config.js import { defineConfig } from 'vite' ; import laravel from 'laravel-vite-plugin' ; export default defineConfig( { ///ここから server: { hmr: { host: 'localhost' , } , } , ///ここまで追加 plugins: [ laravel( { input: [ 'resources/css/app.css' , 'resources/js/app.js' , ] , refresh: true , } ), ] , } ); Tailwind CSS を導入する 以下のコマンドでTailwind CSS をインストールします。 ./vendor/bin/sail up -d sail npm install -D tailwindcss postcss autoprefixer sail npx tailwindcss init -p すると、tailwind.config.jsという設定ファイルが作成されます。 ここで以下のような設定が可能です。 theme:主に色、フォントサイズ、余白などの設定を変更するために使用されます。例えば、ヘッダーの高さやフォントファミリー、リストアイテムのインデント、ボックスシャドウのカラーやサイズなどを指定できます。 variants:各クラスに適用される状態を指定するために使用されます。例えば、hoverやfocus、activeなどの状態に応じたスタイルを定義することができます。 plugins:Tailwind CSS に新しい機能や変数を追加するために使用されます。例えば、フォントサイズを変数にまとめたり、新しい テキストエフェクト を追加することができます。 ここでは、デフォルトのままとします。 tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./resources/**/*.blade.php" , "./resources/**/*.js" , "./resources/**/*.vue" , ] , theme: { extend: {} , } , plugins: [] , } 次にresources/ css /app. css に以下のコードを追加し、Tailwindのプリセットを使用できるようにします。 resources/ css /app. css @tailwind base ; @tailwind components; @tailwind utilities; これらのプリセットを使用することで、プロジェクトにTailwind CSS を適用するために必要な基本スタイル、 コンポーネント 、およびユーティリティクラスがインポートされます。 これでビルド時にTailwind CSS が読み込まれるようになりました。 bladeから CSS を呼び出すと完成です。 <!doctype html> < html > < head > < meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > @vite('resources/css/app.css') </ head > < body class = "bg-gray-400" > < h1 class = "text-3xl bg-red-500 font-bold underline" > Hello world! </ h1 > </ body > </ html > 以下のコマンドを入力すると開発サーバに CSS が配置されます。 sail npm run dev アクセスすると以下のような画面が表示されます。 補足 上記で実行した sail npm run dev は、開発サーバーを起動するためのコマンドであり、プロジェクトのファイルを監視し、変更があった場合に自動的に再ビルドして更新されたコードを提供します。このコマンドは、開発中に使用され、 ソースコード の変更を即座に確認することができます。 本番環境用にビルドする際は、 sail npm run build を使用します。これにより、public ディレクト リにファイルが出力され、本番環境にデプロイできる状態になります。 最後に いかがだったでしょうか。今回はTailwind CSS の導入でしたが、Vue.jsやReactも同じように簡単な手順で導入することができます。 また、Laravel Mixからの移行もできるため、ぜひ試してみてください。
こんにちは 配配メール開発課 Jazumaです。 業務やプライベートでChatGPT等のAIを使いながらもより良い回答を得るためのプロンプトの作り方が分からないという方は多いのではないでしょうか。かく言う私もその一人です。 そこで今回は Best practices for prompt engineering with OpenAI API を見てプロンプトエンジニアリングのベストプ ラク ティスを整理します。 より良い出力を得るためのプロンプト 最新のモデルを使う プロンプトの冒頭で指示を出す・区切り文字で指示と文脈を明確に区切る 具体的に指示を出す・説明する・詳細を記載する 例を提示して出力のフォーマットを明確に示す zero-shotから始め、few-shot, fine-tuneの順に試す あいまいな指示や不正確な指示を減らす 「してほしくないこと」ではなく「してほしいこと」を指示する コードを生成させる際は特定のパターンに誘導するための文言を含める 終わりに より良い出力を得るためのプロンプト 最新のモデルを使う より良い回答を得るために最新のモデルを使うことが推奨されています。2023年4月時点においてはテキスト生成:GPT-4, 画像生成: DALL・E, 音声変換:Whisper が有力な選択肢になると思われます。 1 プロンプトの冒頭で指示を出す・区切り文字で指示と文脈を明確に区切る # 悪い例 Summarize the text below as a bullet point list of the most important points. {text input here} # 良い例 Summarize the text below as a bullet point list of the most important points. Text: """ {text input here} """ OpenAIのドキュメントでは上記のように文脈部分を区切り文字で囲う例が紹介されていましたが、別の資料では指示の部分を「###」で囲っていました。 どちらがより良いプロンプトかは未確認です。 具体的に指示を出す・説明する・詳細を記載する コンテキスト・出力結果・出力の文字数・出力形式等はなるべく具体的に詳しく指定します。 # 悪い例 経費精算システムを紹介するメールを作成してください。 # 良い例 経費精算システムを宣伝するためのメールの本文を作成してください。 詳細:""" 形式:HTML 対象:企業の経理担当者 容量:20KB程度 内容:https://www.rakurakuseisan.jp/ を本文に含める """ 例を提示して出力のフォーマットを明確に示す 出力のフォーマットを具体的に示してあげるとモデルからより良い回答が得られます。 また、出力形式が定まることにより、出力結果をプログラムで解析しやすくなります。 # 悪い例 以下のテキストで言及されているエンティティを抽出します。次の 4 つのエンティティ タイプを抽出します: 会社名、人名、特定のトピックとテーマ。 テキスト: {text} # 良い例 以下のテキストで言及されている重要なエンティティを抽出します。最初にすべての会社名を抽出し、次にすべての人名を抽出し、次にコンテンツに適合する特定のトピックを抽出し、最後に一般的な包括的なテーマを抽出します 望ましい形式: 会社名: <comma_separated_list_of_company_names> 人名: -||- 特定のトピック: -||- テーマ: -||- テキスト: {text} zero-shotから始め、few-shot, fine-tuneの順に試す 前提事項 zero-shot: プロンプトに例を示さずに指示だけを出すこと few-shot: プロンプトに何件か例を提示して指示を出すこと fine-tune: 特定のタスクをより高精度に実施するために、既存のモデルにデー タセット を与えて再学習させること まずは簡単なzero-shotプロンプトで指示を出し、上手くいかない場合はより高度なプロンプトを使います。 あいまいな指示や不正確な指示を減らす 指示を出す際は「なるべく」「少なく」等の形容詞ではなく、具体的な数値を使うことが望ましいとされます。 # 悪い例 楽楽精算についてなるべく短く、いくつかの文で一文が長くなりすぎないように説明してください。 # 良い例 楽楽精算について3 ~ 5分程度で説明してください。1文あたりの文字数は40字程度です。 「してほしくないこと」ではなく「してほしいこと」を指示する # 悪い例 以下は、エージェントと顧客の間の会話です。ユーザー名やパスワードを聞かないでください。繰り返さないでください。 顧客: アカウントにログインできません。 エージェント: # 良い例 以下は、エージェントと顧客の間の会話です。エージェントは問題の調査と解決策の提案を試みますが、個人情報に関する質問は控えます。ユーザー名やパスワードなどの情報を尋ねる代わりに、ユーザーにヘルプ記事 www.samplewebsite.com/help/faq を案内します。 お客様: アカウントにログインできません。 エージェント: ※ 良い例でも「控えます」のようにしてほしくないことを指示しているように見えますが... 否定文ではなく肯定文で指示をすることが重要なのでしょうか。 この点についてはOpenAIのドキュメントには記載されていませんでした。 コードを生成させる際は特定のパターンに誘導するための文言を含める 例えば SQL を生成したい時は「SELECT」、 Python や JavaScript のコードを生成したい時は「import」をプロンプトの末尾に記載すると 期待するコードをモデルが生成しやすくなると言われています。 # 悪い例 # Write a simple python function that # 1. Ask me for a number in mile # 2. It converts miles to kilometers # 良い例 # Write a simple python function that # 1. Ask me for a number in mile # 2. It converts miles to kilometers import 終わりに OpenAIのドキュメントでは「なんとなくこうすると上手くいく」という漠然とした経験則が 言語化 されていました。 業務でAIを使う場合、やはりコードを生成するのが主な用途になると思います。 今後コード生成に特化したプロンプトのベストプ ラク ティスも 調べてみたいと思います。 Models - OpenAI API ↩
2回目の投稿です。rks_mnkiです。 インフラエンジニア的なことをやっています。 さて今回は、「ブラウザ環境上の操作のみでシステムログを取得する環境構築」について、実際の設定内容などを踏まえながらご紹介したいと思います。 目次: 1.導入を進める背景、課題 2.システム要件 3.利用ツールの選定 4.システム全体の構成イメージ 5.各種コンポーネントの説明 Jenkinsジョブ Promtail Loki Grafana Nginx 6.Docker Composeによる実装 7.操作の流れをご紹介 8.まとめ 1.導入を進める背景、課題 弊社では、日々のシステム運用業務の中でインフラ以外の開発やカスタマーサポート部門でも、エラー調査や顧客からの問い合わせ対応などでログを必要とすることがあります。 その際、従来はシステムが稼働する本番環境上にある専用サーバに対して特殊な経路でリモートログインし、以下のような流れでログを取得していました。 取得したい対象サーバ名を記載したリストを作成 定期的に「なんらかの処理」が走って、対象サーバから決められたログが取得されている その中身を確認 ここで「なんらかの処理」と表現したのは、このログを取得する仕組みが大昔に作成されたものであり、正直あまり詳しく理解できていない為です。 また、 Linux 環境上での操作となるために、あまり慣れてない部門のメンバーでは対応しづらく、そのほかの問題もあって利用頻度は低い状態でした。 結局は、インフラ部門に対してログの提供依頼があり、その都度インフラにてログを取得するという対応が発生しています。 課題をまとめると、このようになります。 用意されているログ取得の仕組みが古すぎてメンテナンスされていない 現行の仕組みでは取得対象サーバを登録してから最長5分の待ち時間が発生 Linux 環境( コマンドライン 操作)に慣れていないメンバーには使いづらい ログの容量が大きい環境では処理が タイムアウト になることも  ↓↓ 用意された仕組みは使われず、インフラへのログ提供依頼ケースが多くなる そのため、ログ取得という「生産性の高くない」業務稼働がインフラ部門で一定の割合を占めてました。 あわせて、昔の仕組みを理解して改修するのは嫌だったので、それならイチから新たにログを取得できる仕組みを構築しようと考えました。 2.システム要件 構築するにあたり、まずは以下のようなシステム要件を検討しました。 基本的にブラウザ環境での操作で完結させる 取得したい対象期間を指定可能 利用ユーザが本番環境へのログインは不要 その他、以下の前提条件を定義。 対象サーバは1台のみ指定 各部署単位で必要なログの種類を選定し、それ以外は選択できないようにする これ以外のケースでログが必要な場合は、これまで通り別途インフラへの対応を依頼頂く運用を想定しています。 3.利用ツールの選定 既にインフラでの業務で利用しているJenkinsでのジョブ実行をベースに検討。 Jenkins+各種 スクリプト promtail+loki+Grafana Nginx 今回は、とりあえず取得したログの中身をそのまま閲覧できればいいので、比較的シンプルに導入できそうなツールを検討しました。 4.システム全体の構成イメージ 以下のような構成を検討し、構築を進めました。 システム構成イメージ ログデータを取得するための ダウンロードサイト と、ログ閲覧用サイトは別々で提供するようにしました。 なお、このイメージ図では掲載できていないが、JenkinsジョブのURLなども含めてアクセス先URLリンクをまとめたサイトを1つ構築して、Nginxにて提供しています。 5.各種 コンポーネント の説明 今回、長期間のログに関する傾向分析ではなく、必要な期間に限定したログの内容を参照する目的にフォーカスして構成しています。 Jenkinsジョブ 対象ホストやログ、期間を指定できる ユーザインタフェース を提供 サービス環境内の管理サーバに設置された スクリプト を実行して、必要なログを取得 取得したログを「ログサーバ」に格納 【補足】 指定されたパラメータは、 スクリプト に引き渡されて実行されます。 その中で、以下のコマンドにて「対象期間」に該当するログファイルを抽出しています。 find {対象ログのフルパス*} -newermt '{開始日付} 0:0:0' -and ! -newermt '{終了日付} 23:59:59' この時、ログファイルの「タイムスタンプ(最終更新日時)」をもとに検索をかけるので、ログの中身で記録されている日時ではないことに注意です。 例)1週間単位でログローテーションされているケースが多い -rw------- 1 root root 346K 3月 12 04:02 messages.5.gz -rw------- 1 root root 2.1M 3月 19 04:02 messages.4.gz -rw------- 1 root root 1.9M 3月 26 04:02 messages.3.gz -rw------- 1 root root 2.0M 4月 2 04:02 messages.2.gz -rw------- 1 root root 2.2M 4月 9 04:02 messages.1.gz -rw------- 1 root root 33M 4月 15 18:03 messages 上記における問題あるケースとして、対象期間を【4/4~4/6】で指定してしまうと、該当するタイムスタンプのログファイルが存在しないので、何もアクションされない事になります。 当然、ログの中身としては該当期間のものが出力されていますが、ファイル単位での検索処理となっているため、このような動きとなってしまいます。 そのため、ジョブ実行時にログが存在しないといったエラーになると、以下のようにメッセージを返すようにしています。 #### WARNING #### No data. Please check HostName or change the specified period and try again. Promtail 所定の位置に格納されたログを収集し、Lokiに転送 各ログの種別に応じてタグ付け Timestampの設定 サンプルコード server: http_listen_port: 9080 grpc_listen_port: 0 positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name: messages static_configs: - targets: - localhost labels: job: messages __path__: /mnt/data/collect_logs/*/*/messages* デフォルト設定では、ログを収集した時刻(promtailが読み込んだ時刻)をタイムスタンプとして扱う事になります。これでは、ログが出力されている日時で検索したいケースに対応することができません。 そのため、ログに出力された日時をタイムスタンプとして扱えるように設定します。 具体的には、 timestamp に関する設定によりデータを解析し、Lokiにて保存されたログエントリのタイムスタンプを上書きします。 例)/var/log/messagesの場合:このログに出力されている日時をタイムスタンプとして認識させる。 Apr 9 05:54:04 {ホスト名} systemd[1]: session-51560.scope: Succeeded. pipeline_stages を以下のように設定。 pipeline_stages: - match: selector: '{job="messages"}' stages: - regex: expression: "^(?s)(?P<time>.+? \\d\\d:\\d\\d:\\d\\d):? (?P<content>.*)$" - timestamp: source: time format: 'Jan _2 15:04:05' location: Asia/Tokyo regex にて日時のタイムスタンプを 正規表現 で指定し、 time 変数に格納。 その下部にある timestamp の source で先ほど設定した time 変数を指定し、 format でタイムスタンプ形式を指定。 この辺りは、公式サイトなどを参考にしながらトランアンドエラーにて設定した為、あまり詳しく理解できていませんので、適切な設定方法があればご指摘ください。。 grafana.com Loki ログの集約と検索が行える Grafanaと連携 ※基本的には、configはデフォルトのまま運用。 Grafana 可視化プラットフォーム Lokiから連携された時系列データを処理する Nginx Webによるログの ダウンロードサイト を提供 URLリンクの管理サイトを提供 デフォルトのconfigを利用し、必要に応じてHTMLファイルを作成。 6.Docker Composeによる実装 今回、上記の各種 コンポーネント に関する複数のコンテナを、Docker Composeにより一括で管理しています。 サンプル)docker-compose.yml version: '3' services: nginx: image: nginx container_name: nginx environment: TZ: Asia/Tokyo ports: - "80:80" volumes: - /data/sample-log_collect/dev:/usr/share/nginx/html/download/sample-develop - /data/sample-log_collect/cs:/usr/share/nginx/html/download/sample-support - /log_aggregation/nginx/config/default.conf:/etc/nginx/conf.d/default.conf - /log_aggregation/nginx/config/index.html:/usr/share/nginx/html/index.html - /log_aggregation/nginx/config/main.css:/usr/share/nginx/html/main.css - /log_aggregation/nginx/config/manual.html:/usr/share/nginx/html/manual.html networks: - sample-log_aggr promtail: image: grafana/promtail:2.3.0 container_name: promtail environment: TZ: Asia/Tokyo command: -config.file=/mnt/config/promtail-config.yaml volumes: - type: bind source: /data/sample-log_collect target: /mnt/data/collect_logs - type: bind source: /log_aggregation/promtail/config/promtail-config.yaml target: /mnt/config/promtail-config.yaml networks: - sample-log_aggr loki: image: grafana/loki:2.3.0 container_name: loki environment: TZ: Asia/Tokyo ports: - 3100:3100 command: -config.file=/mnt/config/loki-config.yaml volumes: - /log_aggregation/loki/config/loki-config.yaml:/mnt/config/loki-config.yaml networks: - sample-log_aggr grafana: image: grafana/grafana:latest container_name: grafana environment: TZ: Asia/Tokyo hostname: grafana volumes: - ./data/grafana:/var/lib/grafana networks: - sample-log_aggr ports: - 8080:3000 user: "0:" networks: sample-log_aggr: name: sample-log_aggr ipam: driver: default config: - subnet: 10.***.***.0/24 なお、デフォルトだとdockerのログは docker-compose logs コマンドで確認が必要ですが、正直使いづらく過去ログも保管しておきたいので、syslog経由でログを出力するように設定しています。 以下の設定を各コンテナごとに追記。 logging: driver: syslog options: syslog-facility: daemon tag: log-aggregation/{{.Name}}/{{.ID}} すると、このようなログが生成されます。 ファイル名:/var/log/docker/log-aggregation.log ログの中身: Apr 15 17:46:07 HOST log-aggregation/loki/197fdf4139ac[207994]: level=info ts=2023-04-15T08:46:07.056567716Z caller=table_manager.go:476 msg="creating table" table=index_19073 準備が整い docker compose up -d コマンドにて起動すると、以下のようなプロセスが生成されます。 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ab60c21fffff grafana/grafana:latest "/run.sh" 34 hours ago Up 34 hours 0.0.0.0:8080->3000/tcp grafana ce89712d582a nginx "/docker-entrypoint.…" 34 hours ago Up 34 hours 0.0.0.0:80->80/tcp nginx 197fdf4139ac grafana/loki:2.3.0 "/usr/bin/loki -conf…" 34 hours ago Up 34 hours 0.0.0.0:3100->3100/tcp loki 691b10c43729 grafana/promtail:2.3.0 "/usr/bin/promtail -…" 34 hours ago Up 34 hours promtail 7.操作の流れをご紹介 まず、対象サーバや期間を、取得したいログの種別を選択してJenkinsジョブを実行します。 Jenkinsジョブの入力画面 ジョブが正常実行されたら、ブラウザ上より欲しいログファイルをダウンロードできるようになります。 対象ログファイルをクリックすると、ダウンロードされます。この場合、利用ユーザのローカル環境にログをダウンロードし、 テキストエディタ 等でログを確認するケースを想定しています。 ダウンロードサイト または、Grafanaによるブラウザ上でのログの閲覧も可能です。 ダッシュ ボード画面も用意していますが、左メニューから Explore を選択すると、取り込んだログファイルを指定して、その内容を閲覧することができます。 Grafana_explore さらに、対象期間の絞り込みや、キーワードによる対象ログの抽出も可能ですので、特定ログを調査したい場合に活用できます。 以下の例では、 systemd のワードで抽出しています。 キーワード検索 8.まとめ 使い勝手の良さを意識して、 ブラウザ上での操作で完結する本番システムログの取得環境 の構築に取り組み、なんとか思い描いた形に仕上げることができました。 これにより、 従来の仕組みと比べてお手軽に使えるようになり、今まで一定の頻度で発生していた 「インフラ部門へのログ提供依頼」 の件数が少しでも減少することを期待 しています。 そのためにも、少しでも見やすいマニュアルを作成して、他部門へ展開しています。 ただし、 対象サーバが複数であったり、対象期間が1ヵ月を超えるようなケースにおいては、本システムを使っての取得はNG としているので、まだ一定数以上の依頼は発生する想定です。 また、リリースしたばかりのシステムであるため、今後利用されていく中で様々な不具合や改修ポイントが見つかると考えており、より良い形になるよう今後もメンテナンスしていきたいと思います。 (実際、「あれ…」ってなる箇所を逐次発見しては改修している状況です。) 以上です。本記事が少しでもお役に立てれば嬉しいです。
はじめに こんにちは、imamoto です。 今年も プロ野球 が開幕し、すっかり春だなぁと感じる今日この頃です。 さて今回は、Go言語でのWeb アプリ開発 をした際のチーム内のノウハウを、 GitHub 上で公開してみた話を書いていきたいと思います。 目次 はじめに 目次 公開したGitHubリポジトリの紹介 資料の構成について モジュラーモノリスで実装した背景 ノウハウ公開に至った経緯 おわりに 公開した GitHub リポジトリ の紹介 私はSRE課に所属しているのですが、SRE課ではGo言語でWebアプリを開発することが多いです。 独立した機能を持った複数のGo Moduleを、1つの モノリス なアプリケーションとしてデプロイする、 いわゆる モジュラー モノリス という設計での アプリ開発 にもチャレンジしているのですが、 その開発中にチームとして蓄積した実装ルールやノウハウのうち、汎用化できそうな部分をまとめて、 以下の GitHub リポジトリ 上に公開したので紹介させてください。 github.com この記事では、以下の3点について説明していきたいと思います。 資料の構成について モジュラー モノリス で実装した背景 ノウハウ公開に至った経緯 資料の構成について まとめた内容は、以下の GitHub 上のREADMEファイルを出発点にして学習できるようになっています。 github.com 前提として、Webフレームワークは gin-gonic/gin 、ORMは GORM 、DI ツールは google/wire を使用しています。 また、 3.推奨する学習順序 の項にも記載してありますが、資料は以下の9つの領域で構成されています。 (2023年4月時点での内容であり、予告なく構成を変更する可能性があります) Basics : DDD の基本説明、 ディレクト リ構成等、当資料を参照するにあたって認識いただきたい前提知識を記載しています。 Multi Module Project : モジュラー モノリス 構成のアプリに対して、Go の Workspace 機能を用いて名前解決する方法を記載しています。<3. Layered Architecture : 単一の Go Module はレイヤード アーキテクチャ の構成を取っています。各レイヤーの説明と基本的な実装方法を記載しています。 Dependency Injection : 各レイヤーをまたいだオブジェクトのファクトリ関数を google/wire で生成する手法を記載しています。 Transaction : DB トランザクション の制御の実装方法を記載しています。 Routing And Middleware : Web フレームワーク Gin を用いたルーティングの実装方法を記載しています。 Row Level Security : PostgreSQL の Row Level Security を、ORM の Gorm を用いて実装する方法を記載しています。 Goroutine Job : Goroutine を使った バッチ処理 の実装方法を記載しています。 Unit Test : 単体テスト の基本的な記述方法を記載しています。 DDD 、 レイヤード アーキテクチャ 、 モジュラー モノリス 等の全体的な設計に関わるトピックだけでなく、 DB トランザクション 、 RLS 、 DI 等の具体的な実装に関わるトピックも含まれています。 多くの方に少しでもお役に立てていただくことができますと幸いです。 モジュラー モノリス で実装した背景 今回まとめたノウハウはモジュラー モノリス のアプリケーションであることが前提となっていますが、 設計としてモジュラー モノリス を選択した背景は以下の3点に集約されます。 業務ロジックの複雑度 単一チームでの開発のためMSA化の必要性が低い Go Moduleを使えばシンプルにモジュラー モノリス 構成を実現可能 実現したい業務ロジックがかなり複雑なアプリを開発する必要があったので、 単純な モノリス のアプリケーションにすると複雑度が高くなりすぎるというリスクが開発当初からありました。 そのため、コンテキストマップの設計をすることで適切なモジュール分割を実施し、 実際のアプリケーションの構成と一致させることを目指しました。 ただ、単一チームでの開発で複数のマイクロサービスに分けて開発するとメンテ 工数 だけ増加しメリットがあまり無い点、 Go言語のGo Moduleを連携させればコンテキストマップの構成とアプリの構成を一致させることができる点を踏まえて、 モジュラー モノリス という形を取ることにしました。 ノウハウ公開に至った経緯 私が所属するSRE課は、私を含めメンバー全員がほぼGo言語の開発経験がないところからのスタートだったので、 有識者 がみんなを引っ張っていくスタイルを取ることができませんでした。 そのため、学習・試行錯誤・軌道修正をを繰り返しながらより良い実装の仕方を固めていくという形で開発を進めました。 一方、設計や実装の背景と、それらに付随する用語を理解する必要がある領域が多い構成になっているにも関わらず、 それらの ドキュメンテーション が追い付いていなかったため、 途中から参画してくるメンバーに対するオンボーディングにおいては口頭で説明せざるを得ない状況になっており、 既存メンバーと新規メンバーの間で、重要な概念に対する理解度に開きが生まれてしまうリスクが生まれてしまっていました。 そのリスクを解消するべく、必要なノウハウを汎用的にまとめてチームで共有することにしました。 また、自分たちの試行錯誤の結果を一般に公開することで、他のエンジニアの助けになれば良いなという思いと同時に、 より良い実装方針のフィードバックも外部からいただけるかもしれないという思いもあり、 今回は GitHub 上で一般公開するという結論に至りました。 おわりに 今回は、SRE課におけるGo言語のWeb アプリ開発 のノウハウを公開したお話について書かせていただきました。 もしご興味があれば、公開させていただいた資料をご一読いただき、感想をいただけると嬉しいです。 それでは、また!
楽楽精算開発部の id:smdr9p です。主に Java を使ったサーバーサイドを担当しています。 前置き GoF の デザインパターン はご存知でしょうか。 ご存知の方も多いかと思いますが簡単に説明すると、 GoF の デザインパターン とは Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides の4人、通称 Gang of Four 、略称 GoF によって書かれた書籍、Design Patterns: Elements of Reusable Object-Oriented Software(邦題: オブジェクト指向 における再利用のための デザインパターン )に掲載されている23の デザインパターン のことです。 GoF パターンや単に GoF と呼ばれることもあります。(この記事では以降は GoF パターンと呼びます。) これらは本のタイトルにも示されているとおり オブジェクト指向 プログラミングの領域における デザインパターン をまとめたものですが、この本、そしてこれに掲載された デザインパターン があまりにも有名であるため、単に「 デザインパターン 」と言った場合にこの GoF パターンを思い浮かべる人も数多くいるほどです。 GoF パターンのうち、特に有用性の高いものは言語仕様自体に取り込まれたり、メジャーな ミドルウェア でサポートされたりしているものもあります。 このように GoF パターンは多くの人々によって長年活用され続けてきているものの、 デザインパターン はこれだけ知っていれば十分、というわけにはいきません。なにしろ GoF パターンは書籍が発行されたのが 1994 年、 Java も PHP もリリースされる前であり、それから オブジェクト指向 プログラミング界隈にも様々な知見が蓄積され、新たな常識やベストプ ラク ティスが作られてきました。 デザインパターン についても新たなパターンの発見や既存のパターンの再評価、ブラッシュアップがなされています。 そこで、それらの中から個人的によく見る、または使いやすいパターンを何回かにわけていくつか紹介したいと思います。 前置き Null Objectパターン パターン適用前 Null Objectパターンを適用してみる まとめ 関連するデザインパターン Strategyパターン Stateパターン Compositeパターン 参考文献 Null Objectパターン 今回は Null Objectパターンです。 このパターンを語弊を恐れずに簡単に言うと 「 null をオブジェクトとして扱う 」 です。 これにより null を ポリモーフィズム に組み込むことができ、null に関するさまざまな面倒や障害を避けることができます。 このパターンでは、実際のオブジェクトと同じインターフェースを持つ null オブジェクトを作成します。このオブジェクトは null 値の代わりに使用され、null 値のときに期待される振る舞いをします。これにより、今まで null チェックを行って null の場合はスキップ、null でない場合はオブジェクトのメソッドを実行、のような処理をしていた場面で、null チェックなしにオブジェクトのメソッドを実行することができるようになります。 パターン適用前 以下の ありがちな コードを Null Object パターンを使って改善してみます。 /** インターフェース */ public interface Shape { void draw(); int sumOfInteriorAngles(); } /** 実装クラス1 */ public class Triangle implements Shape { public void draw() { System.out.println( "△" ); } public int sumOfInteriorAngles() { return 180 ; } } /** 実装クラス2 */ public class Square implements Shape { public void draw() { System.out.println( "□" ); } public int sumOfInteriorAngles() { return 360 ; } } /** Factoryクラス */ public class ShapeFactory { public static Shape createPolygon( int numOfCorners) { return switch (numOfCorners) { case 3 -> new Triangle(); case 4 -> new Square(); default -> null; } } } 図形を表す Shape インターフェースの実装として、 Circle クラスと Square クラスがあります。 図形を描画する draw() メソッドに加え、図形の内角の和を取得する sumOfInteriorAngles() メソッドも持っています。 またそれらの実装クラスを生成する ShapeFactory クラスもあります。角の数を指定するとそれに応じた図形クラスの インスタンス を返してくれますが、対応する実装がない場合は null を返します。 そして以下がそれらのクラスを利用する利用側のクラスです。 /** 利用側クラス */ public class Main { public void drawAll(List<Shape> shapes) { for (Shape shape : shapes) { if (shape == null ) { // nullチェック continue ; } shape.draw(); } } public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .mapToInt(Shape::sumOfInteriorAngles) .sum(); } } 図形リストの shapes を渡してリストの図形を描画したり、リスト全体の内角の総和を取得できたりします。 ここで渡されるリストの生成方法については記載していませんが、リストの要素はすべて ShapeFactory クラスによって生成されたものが入っていると考えてください。 上記のとおり ShapeFactory クラスは null を返すことがあるのでリストには null が含まれている可能性があります。リストから取り出した Shape の実装クラスのオブジェクトはしっかり null チェック して、「 null の場合は何もしない 」ようにしています。 …のつもりだったのですが、実は一箇所 null チェックを忘れている箇所がありました。 public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .mapToInt(Shape::sumOfInteriorAngles) .sum(); } このメソッドに渡されるリストにも null が含まれる可能性がありますので、ここでも null チェックが必要でした。 public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .filter(Objects::nonNull) // ここも要nullチェックでした .mapToInt(Shape::sumOfInteriorAngles) .sum(); } このケースではここだけの修正で済みましたが、他の Shape を参照している箇所で同様のチェック漏れがないか確認する必要がありそうです。また、 Shape に新たなふるまいが追加された場合にも継続して注意していく必要がありますし、 Shape 以外のクラスでも類似の例がないか確認する必要もあるかもしれません。 このように、null を null のまま使い続ける限り、常に null を意識してコードを書いていく必要があります。 しかも null チェックを忘れていたとしても コンパイラ はエラーを吐いてはくれず、実行時に条件が合った場合に初めて NullPointerException が発生するのも頭の痛いところです。テストコードでチェックするとしてもテストケースから抜けていたら同じことです。 Null Objectパターンを適用してみる このような場合に Null Objectパターン が役立ちます。このパターンを適用することで、利用側で null チェックが不要になり、コードがシンプルで読みやすくなるだけでなく、今後の機能追加や変更に対しても安全性が向上します。 それでは、上記の実装に Null Objectパターンを適用してみましょう。 Null Objectパターンでは、null の場合に適用されるクラスを作成し、いままで各利用箇所に書かれていた「 null の場合のふるまい 」をあらかじめ持たせておきます。 Shape であれば draw() のときは文字どおり「何もしない」ふるまいをします。 sumOfInterirorAngles() であれば「何もない」数値である0を返すふるまいをします。 /** Null Object実装クラス */ public class NullShape implements Shape { public void draw() { // 何もしない } public int sumOfInteriorAngles() { return 0 ; // 何もない } } そして、 Factory クラスは適切な Shape の実装クラスの インスタンス を生成できない場合、Null Object である NullShape の インスタンス を返すようにします。 /** Null Object対応 Factoryクラス */ public class ShapeFactory { public static Shape createPolygon( int numOfCorners) { return switch (numOfCorners) { case 3 -> new Triangle(); case 4 -> new Square(); default -> new NullShape(); // ここが変わった } } } ※ ここでは簡易的に都度 new していますが、可能であれば Singletonパターンで NullShape の インスタンス は1つのみ生成されるようにしておくのが望ましいです。 これによりリストには null の代わりにこの NullShape オブジェクトが入るようになりますので、リストから取り出した Shape の null チェックは不要になります。 /** Null Objectの恩恵を受けた利用側クラス */ public class Main { public void drawAll(List<Shape> shapes) { for (Shape shape : shapes) { // if (shape == null) { // nullチェック不要! // continue; // } shape.draw(); } } public int countCorners(List<Shape> shapes) { return shapes.stream() // .filter(Objects::nonNull) // ここもnullチェック不要! .mapToInt(Shape::numOfCorners) .sum(); } } もちろんこの変更によってエラーは出ませんし、出力結果も変わりません。 いままでは利用側のコードで「オブジェクトが null であれば何もしないようにする、null でなければオブジェクトに命令する」判断が必要でした。 しかし Null Objectパターンを適用したコードでは、 利用側は「オブジェクトに命令する」のみ でOKです。オブジェクトが Null Object でなければ今までと同じふるまいをしてくれますし、Null Object であれば 「何もしない」をしてくれます 。 このように null を NullShape として Shape の派生とすることで、null を特別扱いすることなく他のクラスと同等の扱いができるようになります。将来的に Shape を利用するコードが追加されたとしても null に関する問題が発生する可能性を軽減できるでしょう。 まとめ このパターンは、null チェックが頻繁に行われるプログラムや、null が意図しないエラーやバグの原因となりやすい場面で特に有用です。 Null Objectパターンは null すらオブジェクトにすることによって ポリモーフィズム の活用範囲を広げる、非常に オブジェクト指向 らしいパターンだと思います。 あなたの既存のコードで Null Objectパターンが活用できる場所がないか、ぜひ探してみてください。 関連する デザインパターン Null Objectパターンに関連、あるいは活用できる他の デザインパターン をいくつか紹介します。 Strategyパターン Strategyパターンは アルゴリズム や実装を実行時に柔軟に切り替えることができるようにするパターンです。Strategy として何もしたくないケースがある場合に Null Object で Strategy を実装する事が可能です。開発やテストのときにログが出力されないようにするために NullLogger のようなクラス使ったことがあるかもしれませんが、それがまさに Null Object です。 Stateパターン Stateパターンはオブジェクトの内部状態にもとづいてオブジェクトのふるまいを変更するパターンです。State が何もない状態のときのふるまいを Null Object に実装することが可能です。初期化前などの状態に null を使用しているのであればそれを Null Object にしてみるといいかもしれません。 Compositeパターン Compositeパターンは階層構造を表現するパターンで、容器と中身を統一的に扱えるようにします。中身が何もないケースを Null Object で実装することで、容器の中身が何もない場合も変わらず統一的な操作を行うことが可能になります。 参考文献 リファクタリング -プログラムの体質改善テクニック Martin Fowler:著 児玉 公信 / 平澤 章 / 友野 晶夫 / 梅沢 真史:翻訳 ピアソン・エデュケーション オブジェクト指向 における再利用のための デザインパターン (改訂版) Erich Gamma / Richard Helm / Ralph Johnson / John Vlissides:著 吉田 和樹 / 本位田 真一:監修 SBクリエイティブ 増補改訂版 Java 言語で学ぶ デザインパターン 入門 結城 浩:著 SBクリエイティブ
配配メール開発課moryosukeです。 2023/03/23(木) ~ 03/25(土)の3日間に渡ってPHPerKaigi2023が開催されました。 今回も前回に引き続きハイブリッド開催となり、現地・配信ともに大盛況でした。 このイベントは 日本PHPユーザ会 主催のイベントで、 ラク スはスポンサーとして協賛させていただいています。 https://phperkaigi.jp/2023/ ラク スからは7人が登壇した他、多くのメンバーが参加しました。 そこで今回は参加者によるレポート、そして ラク スからの登壇者本人によるレポートを紹介させていただきます。 3/23(木)前夜祭 名著「パーフェクトPHP」のPart3に出てきたフレームワークを令和5年に書き直したらどんな感じですかね? レポート ある日オレオレフレームワークを作りたくなったぞ!! レポート 名付けできない画面を作ってはならない - 名前を付けるとは何か レポート 3/24(金) 1日目 作って理解するバックドア レポート 時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる レポート 防衛的 PHP: 多様性を生き抜くための PHP 入門 レポート 詳説「参照」:PHP 処理系の実装から参照を理解する レポート Composerを「なんとなく使う」から「理解して使う」になる レポート PHP Parserで学ぶPHP レポート 安全にプロセスを停止するためにシグナル制御を学ぼう! レポート パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 発表者本人によるレポート 不幸を呼び寄せる命名の数々  ~君はそもそも何をされてる方なの?~ 発表者本人によるレポート stdClassって一体何者なんだ?! 発表者本人によるレポート 特徴、魅力を知って、各PHPフレームワークを使いこなそう! レポート 3/25(土) 2日目 実例から学ぶ変化に強いテーブル設計 - 責務の分解とRDBMSの上手い使い方 レポート いろいろなフレームワークの仕組みを index.php から読み解こう レポート 約10年もののPHPアプリケーションとの付き合い方と、今後10年改善し続けるための取り組みについて レポート 計測できるレガシーさを捉え、コード改善に対処する レポート 【実録】「PHP_CodeSniffer」で始める快適コードレビューライフ 発表者本人によるレポート PHPマジックメソッドクイズ! 発表者本人によるレポート フレームワークが存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 レポート まとめ PHPerのためのコミュニティ PHPTechCafe 3/23(木)前夜祭 名著「パーフェクト PHP 」のPart3に出てきた フレームワーク を令和5年に書き直したらどんな感じですかね? report by id:rakuinoue きんじょうひできさん (@o0h_) による発表です speakerdeck.com レポート 2010年頃、同様に MVC フレームワーク を作成し社内に広めていた身からすると、非常に刺さる内容でした。 当時と比べると、 Composerの登場・PSR-xxなどの デファクト 化が進む PHP のバージョンアップに伴う型に関するサポート・静的解析の普及 フルスタ ックより、様々なライブラリを組み合わせていく これに伴う抽象化やDIの概念の必要性 等が変わってきた内容として挙げられていました。 改めて設計技法等含めて、おさらいできる良い内容だったと思います。 ある日オレオレ フレームワーク を作りたくなったぞ!! report by id:akikuchi_rks 果物リンさん (@FruitRiin) による発表です レポート 独自で作成した フレームワーク 、いわゆるオレオレ フレームワーク を作成した過程をお話しいただきました。 composer init で フレームワーク の雛形を作成するところからPakegistに公開するところまでプロジェクト作成の全体の流れを知ることができ、とても勉強になりました。 また、発表の冒頭ではオレオレ フレームワーク の歴史的背景が語られていたのですが、2000年代初頭の今普及しているような フレームワーク が登場していない頃には 当たり前のようにオレオレ フレームワーク が作られていたというお話には衝撃を受けました。 登壇者もお話ししていたようにオレオレ フレームワーク はセキュリティの問題や引継ぎが大変という理由から現代のプロダクションに活用するのは難しいと思いますが、 今回のように フレームワーク を1から作ってみることは普段意識していない フレームワーク の内部的な仕組みを知ることができ、かなり役立つのではないかと感じました。 名付けできない画面を作ってはならない - 名前を付けるとは何か report by id:mrstsgk_rks ちゃちいさん (@chatii) による発表です speakerdeck.com レポート トーク 名は「名付けできない画面を作ってはならない」ですが、画面についての話ではなく 命名 についての話になります。 名前をつける大切さが伝わる発表でした。特に名前を付けて認識を共有するについて、 ユビキタス 言語の例は納得できました。 また、技術的な発表で心理学の概念(愛着理論)が出てきたのは面白かったです。 最後に、リーダブルコードはやっぱり良書でした。 3/24(金) 1日目 作って理解する バックドア report by id:hirobex Rokuさん (@ad5jp) による発表です speakerdeck.com レポート バックドア を設置されるとどうなるのか なぜ バックドア が設置されてしまうのか バックドア の攻撃手法 バックドア を設置されないようにするには といった、 バックドア に関する知識が詰まった トーク でした 中でも、実際に採集された バックドア のサンプルは一見の価値ありだと思います! 時間を気にせず普通に カンニング もしつつ ISUCON12 本選問題を PHP でやってみる report by id:hirobex sji さん (@sji_ch) による発表です speakerdeck.com レポート ISUCON12優勝チームのレポジトリを参考にしつつ、 PHP でハイスコアを目指すという内容でした データベースの ボトルネック 解消後、徐々にGoと引き離されて行く中で、 いかに PHP としてアプリケーション側のスコアを上げていくのかという挑戦が非常に面白かったです Webアプリケーションパフォーマンスに興味のある人は、ぜひ見るべき トーク だと思いました! 防衛的 PHP : 多様性を生き抜くための PHP 入門 report by id:hirobex しけちあさん (@s6n_jp) による発表です レポート 多様性を生き抜くためにどのような防衛手法が取れるのか、という話から始まり PSR-12などの規約や、様々な静的解析ツールの使い方を紹介していただきました コード品質を担保するために、静的解析を使ってみたい人におすすめの トーク です! 個人的に、この トーク で紹介されていた「Rector」というツールが気になりました✍ 詳説「参照」: PHP 処理系の実装から参照を理解する report by id:mrstsgk_rks nsfisisさん (@nsfisis) による発表です github.com レポート PHP における参照の不思議クイズから始まり、 PHP の内部でどのような処理が行われているかの解説を挟んで クイズでの参照の挙動を説明していただき、 PHP における参照を少し理解できました。 特に、 PHP 処理系は C言語 で書かれていますが、 C言語 の知識はあまり必要ではなかったので C言語 を触ったことのない私からすれば、発表が聞きやすかったです。 Composerを「なんとなく使う」から「理解して使う」になる report by id:hiro_ji あすみさん (@asumikam) による発表です speakerdeck.com レポート Composerではどのように依存関係を管理しているかや、 DockerにおけるComposerの取り扱いについてお話しいただきました。 Composerを理解して使うためにはまずcomposer. json /composer.lockがいつ更新されるかを理解する 本番環境に余計なものを入れないためには Copmoser install に[--no-dev]オプションをつける マルチステージビルドでDockerイメージを作成 タイトルにもあるようにComposerを「なんとなく」使っていた身としては必見の内容でした! PHP Parserで学ぶ PHP report by id:takaram inouehiさんによる発表です speakerdeck.com レポート PHP コードを 構文解析 して抽象 構文木 (AST) に変換できるライブラリ PHP -Parser ( nikic/php-parser ) を通して、 PHP 自体について学ぼうという発表です。 PHP -Parserは PHP 本体のコントリビューターでもある Nikita Popov さんが開発しており、PHPStanやPsalmといった広く使われている静的解析ツールも PHP -Parserの上に成り立っています。 普段何気なく書いたり読んだりしている PHP ですが、文と式の違い、関数と言語構造の違い、 スカラー 値の種類など、把握している方はどれくらいいるでしょうか? 私はそれなりに知っているつもりでいましたが、「そんな書き方できたんだ!」ということもあり 目から鱗 でした。 PHP 自体は C言語 で実装されていて、なかなか中身を読むのはハードルが高いですが、 PHP で実装された PHP -Parserは、 PHP を詳しく学びたい人にはちょうどいいと感じました! 安全にプロセスを停止するためにシグナル制御を学ぼう! report by id:hirobex やまもとひろやさん (@HiroyaYamamoto1) による発表です speakerdeck.com レポート プロセスとシグナルの関係を、なんとなくで理解している人におすすめの トーク です! 私自身、とりあえず Ctrl+C でプロセスが止まるんでしょ?といった程度の理解だったのですが この トーク を聞いてプロセスの止め方に適したシグナルがあることや、プロセスの止まり方によって PHP プログラムが制御できることを知りました! パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 report by id:taclose 弊社 前田啓佑 (@taclose) による発表です speakerdeck.com 発表者本人によるレポート パフォーマンス改善の失敗談と成功談を話した内容です。 改めて「計測するのが大事なんだな」と理解してもらえたかと思います。 スライドのほとんどは非エンジニアでも読めるような構成をとっており、若手やマネージャーの方でも読める構成になっています。 今一度パフォーマンス改善のやり方をチーム内で話し合って、共通認識を持ってもらえればと思います。 不幸を呼び寄せる 命名 の数々  ~君はそもそも何をされてる方なの?~ report by id:yamamuuu 弊社 山村光平 (@yamamu096454848) による発表です speakerdeck.com 発表者本人によるレポート 開発をしていると「この変数は何を表しているのだろう?」とか、「この関数は何をしているのだろう?」とか、一目見ただけでは何を伝えたいのかが分からない 命名 を目にすることがあります。 意図が伝わらないだけであれば調べれば済むかも知れませんが、時には 命名 が誤解を招き大問題に発展することもあります。 そんな不幸を呼び寄せる 命名 をあなたも知らず知らずのうちにしているかも知れません。 スライドでは「君はそもそも何をされてる方なの?」と言いたくなる 命名 の数々を紹介し、改めて 命名 の重要性を感じていただきます。 stdClassって一体何者なんだ?! report by id:daina_rks 弊社 寺西帝乃 (@dainabook) による発表です speakerdeck.com 発表者本人によるレポート stdClassを使う場面はよくあるかと思いますが、「そもそもこのクラスは一体何者なのか」と質問されると答えに困るPHPerは多くいるのではないでしょうか。 この発表ではstdClassの生成方法 ⇒ stdClassにできること・できないこと ⇒ stdClassに関する議論の順番で、「stdClassとは一体何者なのか」の答えに近づく内容です。 PHP8.2で導入される「動的プロパティの廃止」にも関連する内容となっております。 stdClassがよくわからない初心者PHPerや、今までなんとなく使ってきた中堅PHPer、stdClassのあり方について考えるベテランPHPer達の理解の手助けになると思います。 特徴、魅力を知って、各 PHP フレームワーク を使いこなそう! report by id:radiocat 弊社 浅野 仁志 (@hitoshi_a0) による発表です speakerdeck.com レポート フレームワーク 選定は「冒険を共にするパートナー選びのようなもの」とのことで冒険のパートナーになぞらえて PHP フレームワーク が紹介されました。 Laravel:多くの人に頼られる人気者、能力も高く王道の主人公 CakePHP :真面目、曲がったことが大嫌い、正義感が強い委員長 Yii:玄人好みの影の立役者、 切れ者 軍師 Slim:最低限の荷物だけで旅をする自由人 永遠の 宗教戦争 的テーマなので「あの フレームワーク は?」という意見も多々あり、反響のあまり発表者が「 Symfony はすみません」とコメントするなど、会場一体の議論を巻き起こすキラーLTとなっていました。また、会場だけでなくオンラインからもコメントが多く寄せられて、会場で流されていたニコ生のコメントともコラボした盛り上がりを見せていました。 SNSでの盛り上がり も合わせてお楽しみ頂ければと思います。 3/25(土) 2日目 実例から学ぶ変化に強いテーブル設計 - 責務の分解と RDBMS の上手い使い方 report by id:hirobex 曽根 壮大さん (@soudai1025) による発表です レポート 実際にやりがちな設計はこうだけど、こう変更が加わったときに辛いよね、だからこう設計すると楽だよね といった、実例から正しいテーブル設計を紹介する流れが非常に勉強になりました 今楽だからとりあえず既存テーブルにカラム追加しちゃう といったことは、私自身やりがちなので気をつけようと思いました いろいろな フレームワーク の仕組みを index. php から読み解こう report by id:hirobex おかしょい/岡田 正平さん (@okashoi) による発表です speakerdeck.com レポート 「 フレームワーク がなぜWebページを表示できるのか」という話を PHP でWebページを表示する方法から始まり、 CakePHP 、Laravel、Slim、 Symfony と様々な フレームワーク のindex. php を読み解いてWebページの表示の仕組みを解説する トーク です 意外と、 フレームワーク しか触ったことのない人におすすめの トーク だと思いました! 約10年ものの PHP アプリケーションとの付き合い方と、今後10年改善し続けるための取り組みについて report by id:Jazuma たけてぃさん (@takeokunn) による発表です レポート 物流システムという商材の特性上、システム障害を起こしてはいけないという要件が存在します。 その制約をクリアするためにSREチーム主導で以下のような取り組みが行われました。 Datadogによるシステムモニタリング。 アラートが発生した場合、Slackに通知する仕組みも構築 JobサーバをDockerizeしてECS化。 これによりMWバージョンアップ等のメンテナンスコストが低下した gitと AWS の権限管理 PHPUnit の高速化 <感想> メトリックスの監視やMWバージョンアップ等システム運用に必須の作業を効率化するための 施策に先手を打って取り組まれているという印象を受けました。 計測できるレガシーさを捉え、コード改善に対処する report by id:Jazuma 大橋 佑太さん (@blue_goheimochi) による発表です レポート コードを「計測」することで効果的な リファクタリング ができるという視点で コードのレガシーさを測定する方法が紹介されました。 重複コードはどれくらいあるか phpcpdというツールにより計測可能。重複コード = 悪というわけではないため検知されたコードの目視確認が必要 循環的複雑度はどのくらいか terryyin/ lizard により計測。(if/else, for, switch等の数を計測する。 これらはコードがどれだけ複雑化の指標となる) 未使用コードの検出 ツールは様々なものがあるため複数のツールを試して比較してみると良い ファイルの変更回数 Gitコマンドでファイルの変更回数を測定可能。変更回数が多い = 責務過多の可能性がある、あるいは価値検証の場合がある Gitコマンドでファイルの変更回数を測定するのは特別なツールを導入することなく実現できるので測定の第一歩として適切だと思いました。 【実録】「 PHP _CodeSniffer」で始める快適コードレビューライフ report by id:mrstsgk_rks 弊社 森下 繁喜による発表です speakerdeck.com 発表者本人によるレポート 私の所属する配配メール開発課の事例をもとに PHP _CodeSnifferを導入したときの恩恵と思わぬ落とし穴を説明しました。 オンライン登壇で ニコニコ生放送 と発表時の音声を片耳ずつ聞いていたので、 音声のラグでスタートに戸惑いましたので、次はオフライン登壇でリベンジしようと思います。 次回「 PHP _CodeSnifferをPhpStormに設定したら実装 工数 が減少できた件」でお会いしましょう!!! PHP マジックメソッドクイズ! report by id:Y-Kanoh 弊社 加納悠史 (@YKanoh65) による発表です speakerdeck.com 発表者本人によるレポート 「このメソッド、定義されていないはずだけど... あ、マジックメソッドを呼び出しているのか」Laravel のコードを読んでいるときに、一瞬迷ったのがこの発表の元でした。 知っていれば「ああ、こういうことね」と脳内で変換できますが、知らないと関係性がわからなくなってしまうのがマジックメソッドだと思います。特に一般的な開発時には自分で実装することが一部を除き多くはないため、まずは知ることが重要です。 このクイズで興味を持った方は、ぜひ一度調べてみてください。 フレームワーク が存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 report by id:radiocat 弊社 廣部 知生 (@tomoki2135) による発表です speakerdeck.com レポート 20年モノのレガシープロダクトの新UI導入をきっかけにLaravelを導入した事例の紹介です。 Laravelに”載せる”実装戦略として、 アーキテクチャ に ADR パターンを採用して既存コードをできる限りそのまま維持してLaravel上で動作させる戦略が紹介されました。 この戦略によって、載せ替える前の構造を維持したままスピーディーな移植を実現できたようです。また、移植によってデータが返り値となる仕組みになったことでテストが書きやすくなりました。あくまで移植しただけなのでまだまだ課題はあるようですが、レガシープロダクトにモダンな フレームワーク を導入する参考事例のひとつとして大きな反響があり、LTではなくレギュラー枠でじっくり聞きたいという意見もあがっていました。 まとめ 初心者向けの内容からマニアックな内容まで幅広い人が聴いていて楽しいイベントとなっていました。LTの時間は現地ではペンライト、配信では色付きのコメントが流れるなど大盛りあがりでした。 PHPerのためのコミュニティ PHPTechCafe ラク スでは PHP に特化したイベントを毎月開催しております。その名も「PHPTechCafe」!! 次回は4/25(火)に『「PhpStormを語る」』 をテーマに開催します! まだまだ参加者を募集していますので、ぜひお気軽にご参加ください。 👉 PHPerのための「PhpStormを語る」 PHP TechCafe 参加申込は以下フォームよりお願いします! rakus.connpass.com 最後までお読みいただきありがとうございました!
1. はじめに 2. Google Homeのしくみ Home Graph について 3. Local Home SDK とは Local Home SDK を使って、やろうと思ったこと 4. Local Home SDKの導入手順 ①あなたが始める前に ②入門 ③スターター アプリを実行する ④クラウド フルフィルメントの更新 ⑤ローカル フルフィルメントを構成する ⑥ローカル フルフィルメントの実装 ⑦スマートウォッシャーを開始する ⑧TypeScript アプリをデバッグする ➈おめでとう 5. 実装手順 任意のタイプの家電を登録する デバイスに備わってる機能を変更する SYNC:追加された機能の詳細 Query:現在の家電の状態を報告する EXECUTE:追加される「EXECUTE」リクエスト に対応する 6. さいごに 1. はじめに 家電にはリモコンが欠かせません。 テレビ、エアコン、照明など、私たちの生活に密接に関わっている家電製品は、その操作のために専用のリモコンが必要になります。 私の場合、テレビにブルーレイレコーダーとスピーカーを繋いでいたため、リビングの机の上に3つのリモコンを常設していたのですが、リモコンを置く場所をとったり、リモコンを見失ったり と、解決したい問題が発生していました。 そこで、SwitchBotのリモートリモコンを使い、リモコンをアプリ管理に変更することにしてみました。 SwitchBotのリモートリモコンとは、家電製品を スマートホーム に対応させるための便利なデ バイス です。 このリモコンは Wi-Fi に接続してネットにつながることができ、事前にリモコンの赤外線を学習させることで、対応する スマートフォン のアプリを介して家電を操作できるようになります。 ただ、SwitchBotを使っていても、アプリをタップしてからリモコン操作が可能になるまでに時間がかかり、不便を感じていました。 そこで、 Google Home を使って 声で命令を出せるようにしたのですが、それでも、下記のような不満を抱えてしまいました。 (微妙な言葉の揺れを吸収してくれたりしたので、機能性に関しては満足しました。) 私が使用している照明は、リモコンの「電源」ボタンを押下すると、ON→常夜灯→OFF→ONのサイクルで状態が変わります。 Google Home は、ONとOFFのステータスしか管理しないため、電気を消したいと思ったとき、 ・「OK、 Google 、照明を消して」-> 照明が常夜灯になり、 Google Home 上のステータスは「OFF」になる ・もう一度「OK、 Google 、照明を消して」 -> 照明がOFFになるが、 Google Home 上のステータスは「ON」になる といったことが起きました。 照明を消すために、「OK、 Google 、照明を消して」を2回いう必要があり、その上、ステータスもずれてしまうのです。 ほかにも、スピーカーの音量を調節できない、扇風機の風量を変更できない等、細かい部分で不満点が残りました。 調べていると、 Google Assistantのプロジェクトを自作することで、家電を操作することが可能であることを知りました。 自分で家電の操作のロジックを作ることができるのであれば、抱えている不満点を解消できるのではと思い、実践してみました。 本記事では、Local Home SDK を用いて、実装を行ったので、その手順を紹介しようかと思います。 詳細は後述しますが、上記を利用すると 家電を操作するために作成するコード、アプリを外部に公開する必要がなくなり、私にとって大きなメリットに感じたため採用しました。 もし悪用されて、家の家電が見知らぬ人によって勝手に動き出すことを考えると、公開するべきじゃないと思いました、、 また、サンプルのGit プロジェクトがあるため、迷う時間を短縮できるとも思いました。 該当のサンプルは、Local Home SDK を使わないパターンのコードも含まれているため、「コードを外部に公開することに躊躇いがない」場合も、そちらを流用できます。 2. Google Home のしくみ まず、 Google Home がスマート家電を操作するときの仕組みを理解しておく必要があるので、簡単に説明します。 ①例えば、 Google Home に「OK、 Google 、照明を消して」と命令したとします。 ② Google Home は受け取った音声を、 クラウド 上の Google Assistant に投げます。 ③ Google Assistantが家電操作の命令と判断した場合、受けとったワードに関連するスマート家電情報をHome Graphから取得します。 Home Graph について 急に現れたワード、"Home Graph" についての説明を簡単に挟みます。 Home Graphとは、端的に言うと Google Home が操作可能な家電を管理するデータベースのようなものになります。 Google Home と対応したプロジェクトの連携を行ったとき、そのプロジェクトが管理している スマートデバイス リストを取得し、保持します。 調べたところ、「ライト」「照明」 などといった言葉の揺れを吸収しているのが、Home Graphのようです。 複数のライトが登録されている時、「リビング」の Google Home から「ライトをつけて」とだけ命令しても、暗黙的に「リビングのライト」を選択してくれます。 Home Graph の詳細 developers.google.com ④Home Graph から受け取った情報と紐づくプロジェクトに対して、実際に家電を動かすように「EXECUTE」リク エス トを送ります。  例えば、受け取った家電情報が SwitchBot で登録した照明だった場合、紐づいているプロジェクトの設定に従って、SwitchBot に向かってリク エス トが飛びます。 ⑤「EXECUTE」リク エス トを受け取ったSwitchBot サービスがからリモートリモコンに対して、「照明の電源ボタンを1回押す」といった命令を渡し、 ⑥それをリモコンが実行することで、照明が消灯(私の場合は常夜灯)します。 もし、ライトがスマート家電だった場合は、リモートリモコンを介さずに照明に対して直接「照明を消す」命令が飛ぶということになります。 以上のような原理で、 Google Home を使って家電を音声操作することができます。 3. Local Home SDK とは Local Home SDK とは Google が提供する技術で、こちらを使うことで Google Home がローカルのネットワークを使って家電に対して、 操作コマンドを直接送信することができるようになります。 developers.home.google.com 図を使って、簡単に説明します。 まず前提として、プロジェクトはあらかじめ「Local Home SDK 」を用いて実装した Google Home 上で動くプログラムファイル(言語はTypeScript系)を用意しておく必要があります。 Google Home は、プロジェクト連携時にそのプログラムファイルを取得します。 ※プロジェクトを紐づけるか、端末を再起動することで自動的に取得する模様 命令を受け取り、Home Graph から家電情報を受け取るところまでは同じですが、ここから先のフェーズが異なります。 ④ Google Assistant は、受取った情報とともに「EXECUTE」リク エス トを" Google Home "に返します。 ⑤「EXECUTE」リク エス トを受け取った Google Home は、あらかじめ取得していたプログラムファイルに従って、 操作コマンドをローカルネットワーク経由で家電に対して直接実行します。 ⑥リモコンが家電を操作する仕組みは同じです。 Local Home SDK を使って、やろうと思ったこと Google Home は定期的にローカルネットワークに対して、操作可能なデ バイス がないか、スキャンして確認しています。 上記に対して、HomeGraph に登録されている家電情報に紐付くIDを返すデ バイス を発見すると、 Google Home はそのデ バイス を該当のスマート家電と認識します。 つまり、同じローカルネットワーク内にあるPCなどで Google Home のスキャンにして、該当するIDを返す仕組みを実装すると、 以降、 Google Home はそのPCをスマート家電と認識するので、「EXECUTE」リク エス トを受け取れるようにすることができるようになります。 ※ラズパイ等でも代用は可能ですが、知識がない(そもそも持っていない)ので、PCを採用しています。 「EXECUTE」リク エス トを任意のデ バイス で受け取ってしまえば、そこから先の処理は自作することができます。 図の通り、SwitchBot API を叩いてスマートリモコンを経由で家電の操作も可能になるということです。 上記のような仕組みを利用することで、先述した通り 家電を操作するアプリを外部に公開する必要がなくなります。 4. Local Home SDK の導入手順 最初に話した通り、Local Home SDK のサンプルコードを使って実装の準備を進めます。 Google 公式の CodeLabs もあるので、その手順に従って、準備します。 developers.home.google.com 本記事では、上記に書いてある実装・操作手順的な内容はばっさりそぎ落とし、簡単な補足情報をまとめようかと思います。 ①あなたが始める前に 本記事の「2. Google Home のしくみ」「3.Local Home SDK とは」にあたる内容が記載されています。 ②入門 準備として Action on Google と Firebase の初期化を行います。 Firebase を使って、「 Google Assistant のプロジェクト」のアプリ部分を作成します。 Action on Google の設定を完了することで、上記で作成したアプリを「 Google Assistant のプロジェクト」として登録するイメージです。 ③スターター アプリを実行する ここから用意されているサンプルコードを用いて、実際に実装していくフェーズです。 github.com コードの更新がしばらく行われておらず、node のバージョンが古くなっていました。 私の場合、node のバージョンが環境と合わず、バージョンの更新とそれに伴う依存関係の修正を行いました。 ここまでの準備が完了すると、Firebase Functions にデプロイしたプログラムに対して、 Google Assistant から「EXECUTE」リク エス トが送信されるようになります。 既存では、パラメータに応じて Firebase Realtime Database のステータスを変更する 処理が走るのみですが、コードを変更すれば実装可能です。 Local Home SDK の仕組みを使用していませんが、家電を操作する実装を行うことができるようになります。 Local Home SDK の機能を使わなくてもいいのであれば、ここまで準備すれば Google Home を使って家電操作できます。 ・functions > index.js を実装することで、家電操作が可能になります 具体的な実装方法の詳細は、後述します。 Local Home SDK を使った仕組みを目指すのであれば、次へ進みます ④ クラウド フルフィルメントの更新 Local Home SDK を使用するための準備フェーズになります。 Google Home からのスキャンに対して、otherDeviceIds に指定したIDを返すことで、 Google Home は該当のデ バイス だと認識します。 ⑤ローカル フルフィルメントを構成する ここで設定した内容に従って、 Google Home がローカルネットワークのスキャンを行います。 ⑥ローカル フルフィルメントの実装 ここで、Local Home SDK を使った実装になります。 identifyHandler Google Home がローカルネットワークをスキャンし、その結果を処理する箇所になります。 対応しているデ バイス は、otherDeviceIds に対応した ID を返してくるので、それをそのまま返すようにしています。 executeHandler Home Graph からきた「EXECUTE」リク エス トを処理する部分です。 基本的に、 Google Assistant から受け取ったコマンドをそのまま仮装デ バイス に渡すだけで良いと思うので、 ここのコードは公式の内容から特に変更はしなくて良いかと思います。 ここでSwitchBot API を叩くという荒技もできそうですが、やめておきました。 ⑦スマートウォッシャーを開始する node 上で動く 仮想のスマート家電を起動します。 Local Home SDK を使った音声での命令が成功すると、ログにそのことが出力されます。 また、併せて Firebase Realtime Database のステータスも変更します。 ⑧TypeScript アプリを デバッグ する 記載の通り Google Home 上で動くコードは、同じネットワークにつないでいるPCの Chrome ブラウザ > inspect でデバックすることが可能です。 また、 スクリプト を更新した場合、 Google Home を再起動しないとその変更は反映されません。 ➈おめでとう ここまで完了すると、 Google Home と仮想スマート家電との疎通が完了します。 サンプルコードはログを出力するだけですが、コードを変更すれば家電操作ロジックを実装できます。 実装方法の詳細は、次の章で解説します。 5. 実装手順 既存のサンプルコードの状態で扱うことができるスマート家電は「洗濯機」で、その機能も限られています。 もちろん、上記は変更可能です。 ここではその方法と、実装手順を紹介いたします。 任意のタイプの家電を登録する まずは、任意の種類を 家電を登録する方法です。 Google Home からくる、「Sync」リク エス ト に応答することで、任意のスマート家電を登録することが可能です。 サンプルコードでいうと、functions > index.js の onSync で返すオブジェクトが、これにあたります。 家電の種類は、payload.devices.type の値で報告できます。 公式のドキュメントに報告できる種別がすべて乗っています。 developers.home.google.com 上記から一つだけ選択して、報告します。 例えば、照明に変更したい場合は、"action.devices.types.LIGHT"を指定します。 payload: { agentUserId: USER_ID, devices: [{ id: 'washer' , type: 'action.devices.types.LIGHT' , // ここを変更する .... これでアプリ上のロゴもライトに変わり、Home Graph で解釈する言葉の揺れも、照明用に代わります。 言葉の揺れ に関して、payload.devices.name.nicknames を変更することで、任意のワードを登録することができるようになります。 .... name: { defaultNames: [ 'My Washer' ] , name: 'Washer' , nicknames: [ '間接照明' , 'テレビ台の上のライト' , ] , // ここを変更する } , .... 上記のように設定すると、「OK、 Google . 間接照明をつけて」や「OK、 Google . テレビ台の上のライトを消して」と話しかけた場合も、登録した機器が選択されるようになります。 「Sync」リク エス ト で報告する内容の詳細は下記を参考にしてみてください。 developers.home.google.com デ バイス に備わってる機能を変更する こちらは、payload.devices.traits の値を変更することで解決することができます。 デ バイス タイプと同様に、公式対応表を参照します。 developers.home.google.com こちらは、複数指定することができます。 例えば、"action.devices.traits.Brightless"を指定することで 、明るさを調整するための命令ができるようになり、対応する「EXECUTE」リク エス トが送られるようになります。 余談ですが、、 私は、"action.devices.traits.Modes" を乱用しています。 任意の「モード」を追加することができ、様々なことに汎用することができます。 「テレビ」に対して、「 NETFLIX モード」「 YouTube モード」「 Amazon Prime モード」を設定すれば、「OK、 Google . テレビをAmazo Primeモードにして」の一言で、 テレビで Amazon Prime を再生させることも可能になります。 SYNC:追加された機能の詳細 実は、traits で任意の機能を報告するだけでは、機能を追加することができない場合があります。 Volume (音量調整) がわかりやすいので、例としてみてみたいと思います。 developers.home.google.com 「デ バイス の属性」の章を確認すると、「Sync」リク エス ト で次の情報を報告する必要があることがわかります。 volumeMaxLevel:ボリュームの最大値 volumeCanMuteAndUnmute:ミュート機能に対応しているかどうか volumeDefaultPercentage:デフォルトのボリューム値 levelStepSize:ボリュームを相対的に調整するときの変化量 commandOnlyVolume:現在のステータスを報告できるかどうか traits = action.devices.traits.Volume と併せて、上記を報告することで初めて、ボリューム調整機能を追加することができます。 Query:現在の家電の状態を報告する 現在の状態やステータスを、HomeGraph に報告することで Google Home アプリに現在のスマート家電の状態を表示することができます。 これがなくても動かすことは可能ですが、実装することをお勧めします。 サンプルコードでは、functions > index.js の onQuery に実装されています。 return { on: data.on, isPaused: data.isPaused, isRunning: data.isRunning, currentRunCycle: [{ currentCycle: 'rinse' , nextCycle: 'spin' , lang: 'en' , }] , currentTotalRemainingTime: 1212, currentCycleRemainingTime: 301, } ; 報告が必要な key は、報告している traits によって決まります。 traits の詳細のページで、確認することができます。 'action.devices.traits.OnOff'(電源のオンオフ切り替え)と'action.devices.traits.Brightless'(明るさ調整)機能を報告した照明を登録した場合を考えてみます。 action.devices.traits.OnOff https://developers.home.google.com/cloud-to-cloud/traits/onoff?hl=ja#device-states action.devices.traits.Brightless https://developers.home.google.com/cloud-to-cloud/traits/brightness?hl=ja それぞれを確認すると、 on と、 brightness を返す必要があることがわかります。 return { on: true , brightness: 50 } ; ※電源がついており、その明るさが 50% であることを報告している EXECUTE:追加される「EXECUTE」リク エス ト に対応する payload.devices.traits を追加して機能を増やすと、その機能に対応した「EXECUTE」リク エス トを処理する必要が出てきます。 サンプルコードでいうと、app-start > functions > index.js の onExecute、もしくは、virtual-device > washer.js の state(既存の関数名だと微妙ですが)で「EXECUTE」リク エス トを処理しています。 公式のドキュメントを確認することで、どのような「EXECUTE」リク エス トが来るかを確認することができるので、 それを参考に、実装を行います。 "action.devices.traits.OnOff"を例に、私の実装を例として簡単に紹介します。 developers.home.google.com 「ライトをオンにして」と Google Home に話しかけたら、下記のような「EXECUTE」リク エス トを受け取ることがわかります。 { "command" : "action.devices.commands.OnOff" , "params" : { "on" : true } } params > on の値を参照し、下記のように実装すれば、家電の電源を切り替えることができます。 // req = 受け取った「EXECUTE」リクエスト const data = req.body; // コマンド種別を取得する const command = data.command switch (command) { case 'action.devices.commands.OnOff' // 切り替えたいステータスを取得 const isOn = data.params.on; if (isOn) { // 電源を入れるケース switchBotApi.on(); } else { // 電源を切るケース // 電源ボタンを2回押下する switchBotApi.off(); } case 'action.devices.commands.BrightnessAbsolute' // 省略 } オフの時だけ電源ボタンを2回押下するように実装できるので、初めに挙げていた照明のステータスがずれる問題を解決することができました。 ※SwitchBot API の叩き方の詳細は、下記をご覧ください。 github.com 6. さいごに 「OK、 Google テレビをつけて」の指示で、テレビとスピーカーの電源を同時に入れるようにしたり、 「OK、 Google 広告をオフにして」の指示で、 YouTube の広告をスキップさせたりと、好きなように家電をコン トロール することができるようになります。 単に Google Home と、SwitchBot サービスを連携させるだけでは上記は実現できませんし、最初に上げたような不具合が発生したりします。 そこを自分で解決することができたので、試して良かったと感じています。 また、コードを動かすトリガーを声にすることができるので、SwitchBot API に限らず、様々なこと(家電操作以外も)ができそうです。 他になにかできないか模索し、より便利な スマートハウス の構築を目指していこうかと思います。
技術広報の syoneshin です。 いつも ラク スエンジニアブログをお読みいただき、ありがとうございます! ! 今回は2023/2/8に開催した 「RAKUS Tech Conference 2023」の内容を当日発表資料 と共にご紹介します! ◆ 関連記事 昨年度(2022年)の開催レポートは以下をご覧ください tech-blog.rakus.co.jp 【目次】 イベント概要 発表の紹介 オープニングセッション 短納期でも進化をあきらめなかった新規プロダクト開発 フロントエンド横断組織のチームトポロジー ベテラン社員が抜けても若手が成長できるエンジニア組織づくり デザイン組織が社内下請けから脱却するためにやったこと ゼロから始めるクラウドネイティブ 「開発優先」の中で取り組む組織的な新技術への挑戦 クロージング 終わりに イベント概要 日時 :2022/2/8(火) 14:00-18:00 会場 :オンライン 参加費:無料 主催 : 株式会社ラクス開発本部 Twitter ハッシュタグ : #RAKUSTechCon techcon.rakus.co.jp 【イベントメッセージ】 株式会社 ラク スは「ITサービスで企業の成長を継続的に支援します!」をミッションに掲げ、 メール共有・管理システムの「メールディーラー」や、経費精算システムの「楽楽精算」など、 延べ80,000社を超えるお客様に自社開発したサービスを提供してきました。 『RAKUS Tech Conference2023』はこうした今までの取り組みや知見を紹介する、 ラク ス開発本部主催の技術カンファレンスです! 当社ではローンチ20年を超えるプロダクトもあり、決して目新しい技術ばかりを扱っているわけではありませんが、 一人ひとりが地道な" カイゼン "のための努力、そして"挑戦"を続けています。 「日本を代表する」組織を目指し、失敗を恐れずに成長を続けるエンジニア/デザイナーの生の声をお届けします。 発表の紹介 それではここから各発表内容と資料を共有させていただきます! オープニングセッション 登壇:公手 真之 [ 執行役員 開発本部長] オープニングセッションでは、 ラク ス開発本部長の公手より、 以下内容を紹介させていただきました! ラク スについて エンジニア組織について CTOが取り組んできたこと ラク スのエンジニア組織のこれから 開発組織の成長過程で遭遇したさまざまな課題と取組み事例、また進行形の課題と取組み、今後のチャレンジをご紹介しました。 speakerdeck.com 短納期でも進化をあきらめなかった新規プロダクト開発 登壇:松浦 孝治 [楽楽明細開発2課 課長]     川上 正博 [楽楽明細開発2課] 2022年12月現在、約25,000社の方に申込頂いている『楽楽電子保存』サービスですが、初期開発に与えられた猶予は弊社のプロダクト開発史の中で最短となる「半年」という、非常にタイトなスケジュールの中で完遂させる必要がありました。 短期間でサービスインまで導いたマネジメント 中長期的なサービス運用を見据えたゼロベースでの アーキテクチャ ー選定 を中心に、初期開発で特に力を入れた取り組みと今後の展望をご紹介しました。 speakerdeck.com フロントエンド横断組織のチーム トポロジー 登壇:國枝 洋志 [フロントエンド開発課 課長] 設立20年目にして作られたフロントエンド開発組織は発足から2年経ちました。 発足時2名しかいなかったフロントエンドエンジニアは18名に増えましたが、まだまだ人手不足。課題も山積みです。 ベスト・オブ・ブリード戦略で成長してきた様々なサービスに対して、各開発課と共にサービス開発を行うストリームアラインドチームになるのか、フロントエンドの スペシャ リストとして技術支援を行うイネイブリングチームになるのか、横断組織ならではの悩みにどのように取り組んで行くのか、これからの組織設計についてお話ししました。 speakerdeck.com ベテラン社員が抜けても若手が成長できるエンジニア組織づくり 登壇:大塚 正道 [配配メール開発課 課長]     荒巻 拓哉 [配配メール開発課]     久山 勝生 [配配メール開発課] ラク スの開発組織には毎年多数の新卒社員が入社しており、私たちのチームにもたくさんの若手社員が所属しています。 一方で スペシャ リスト職などのベテラン社員は不足気味です。 重要度の高いプロジェクトに専念するためチームから抜けていくこともあります。 しかし、私たちは逆にこれを若手社員の成長の機会と捉えて積極的にチャレンジできる環境をつくり、若手が活躍し成長できるチームを目指して取り組んできました。 実際に取り組みを行ったメンバーからその事例をご紹介するとともに、マネジメントサイドからそのための組織の向き合い方についてお話ししました。 speakerdeck.com デザイン組織が社内下請けから脱却するためにやったこと 登壇:小林 肇 [プロダクトデザイン課 課長] デザイナーが少なく、 開発プロセス の中でデザイナーの役割の定義が曖昧で、デザイナーって何をしてくれる人なのか、プロダクト開発においてデザイナーがいるとどんなメリットがあるのかがちゃんと認識されていませんでした。キレイなUIを作ってくれるということは認知されていても、そのためにはどのようにデザイナーがプロダクト開発に関わるべきか不明瞭なために、下請け的な仕事になっていました。 課題解決をミッションとするデザイナーが、プロダクト開発チームの一員になるために、どのようにして役割を認識してもらい、役割を広げていっているのかをご紹介しました。 speakerdeck.com ゼロから始める クラウド ネイティブ 登壇:見形 親久 [SRE課 課長]     松本 隆二 [東京インフラ開発1課] クラウド ネイティブな開発をやってはみたいが、機能開発が忙しくなかなかそのチャンスが生まれない、そんな時に新しいサービスを立ち上げるチャンスが舞い込んできました。 これ幸いと、イメージしていた構成を構築しようとしてみましたが、あれやこれやとハマりポイントや考慮できてないモノがあり思っていたほど簡単なものではありませんでした。 Kubernetes など クラウド ネイティブ技術が一般的になっては来ていますが、実際に導入するにはエコシステムの構築など、周辺のシステムも考慮する必要があり、どこから手を付けるべきか悩んでいる方もいらっしゃるのではないでしょうか。 システム構成の紹介/その背景 環境構築時のハマりポイント/考慮点 今後やっていきたいこと を中心に、ゼロからどの様に環境を構築していったのかと今後の展望をご紹介させていただきました。 speakerdeck.com 「開発優先」の中で取り組む組織的な新技術への挑戦 登壇:堀内 泰秀 [技術推進課 課長]     鈴木 勇 [技術推進課] 「機能開発を優先しているため リファクタリング ができない」この状態に陥っている方、多いのではないでしょうか。 サービス開発では立ち上げから売れ始めるまでの間、プロダクトマーケットフィットを目指して最短距離を走ることを求められます。 その中で行われる意思決定では必ずしもすべてが最適解を得られるわけではありません。また時を経るにつれて最適解が最適解でなくなることもあるでしょう。 「サービスがある程度軌道に乗り、開発体制も整ってきたら リファクタリング やリアーキテクトしたい」――しかしベテランエンジニアの方はそんなタイミングが訪れないと知っているはずです。 そんな厳しい現実を直視して、 ラク スではどのような対応を取ってきたのかをご紹介しようと思います。 本発表では以下のような内容をお話しします。 ラク スにおける取り組みの概要 取り組みの立ち上がりから継続していくための仕組みづくり 取り組み実施のサイクル 自社で検討することのメリット speakerdeck.com クロージング 登壇:公手 真之 [ 執行役員 開発本部長]     大塚 正道 [配配メール開発課 課長]     小林 肇 [プロダクトデザイン課 課長]     見形 親久 [SRE課 課長]     堀内 泰秀 [技術推進課 課長] クロージングセッションでは、 ラク ス開発本部長の公手と 当日登壇者複数名で 本編では伝えれなかったこと 自身のチームで今後やろうとしていること テーマにパネルディスカッションを実施しました。 イベント当日はたくさんの方にご視聴、そしてコメントやご質問をいただきました。 お申し込み、ご参加いただいた皆さま本当にありがとうございました! 終わりに ラク スMeetupでは現場最前線のエンジニア/デザイナーから ラク スの SaaS 開発ならではの技術・運用ノウハウや、 新しい取り組みの成果や失敗談、プロダクト開発/運用で得た知見等の技術情報をお届けしております。 今後も定期的なイベントを計画しております、ぜひご参加ください。 最後までお読みいただきありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申し込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申し込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
こんにちは、技術推進課の uemura_rks です。 私が所属する技術推進課では「各商材の開発チームから課題を ヒアリ ングし、その解決に向けた調査を行う」という取り組みをしています。 取り組みの一環で Java のパフォーマンス分析、診断ツールである JDK Flight Recorder(JFR) について、その機能と導入することによるアプリへの影響を調査・検証しました。 「小さいオーバーヘッドで使えるらしいけど本当にそうなのか?」 を確認してみたので、その結果を報告します。 JFR を使えばトラブルの 原因調査に役立つ情報 を 手軽に収集 できます。 Java を扱っている方であればどなたにとっても有益なツールとなりますので是非最後までお付き合いください。 目次 目次 JDK Flight Recorder(JFR)の特徴 オーバーヘッドが小さい 手軽に使い始められる JVM やアプリ動作に関する情報を取得できる トラブルの再発を待たなくていい 取得情報はカスタマイズ可能 イベントログのデータフロー オーバーヘッドによる影響度検証 検証結果 CPU Javaヒープ使用量 プロセスが消費する最大メモリ量 ディスク吐き出し時のアプリ動作への影響 ダンプサイズ まとめ 参考文献 JDK Flight Recorder(JFR)の特徴 検証の前にまずはJFR がどんなツールなのか、その特徴を紹介します。 オーバーヘッドが小さい アプリケーションを分析、診断するためには多くの情報を計測する必要があります。 JFR は JVM に組み込まれている ため、その計測のオーバーヘッドは小さく、1,2%程度だといわれています。 商材に導入したらアプリ全体のパフォーマンスが悪化した、なんてことが起きにくいです。 (と、言いつつこの部分を後半で検証します) 手軽に使い始められる JFR は各ベンダーの JDK に実装されているので、現在使っている JDK はそのまま、 機能を有効化するだけ で利用可能です。(有効化には JVM の起動オプションや jcmd を使います) アプリサーバに何かしらの ミドルウェア をインストールしたり、 ソースコード に手を加えたりすることなく使い始められます。 JVM やアプリ動作に関する情報を取得できる JFR のログからは JVM のリソース状況 GC の情報 スレッドダンプ コンパイル やコード・キャッシュの情報 ログを自動分析したレポート など様々な情報を得ることができます。 JVM の動作に関するような低レベル情報を集めてくれるので、トラブル調査に役立つと思います。 いくつか例を載せておきます。 < JVM のリソース状況> CPU 使用率やヒープ使用量、オブジェクトのメモリへの割当てなどを確認できます <自動分析のレポート> ログの包括的な分析レポートです。 潜在的 な問題をスコア化して警告してくれます。 <スレッドダンプ> 1分間隔でスレッドダンプを記録してくれています。 ◆ログの可視化 JFR のログ自体はバイナリになっています。 JDK Mission Control(JMC) というビューアーを使うと上記キャプチャのように可視化できます。 その他、JFR で得られる情報やその見方についてはこちらの チュートリアル が分かりやすいです。 トラブルの再発を待たなくていい JFR は基本的に 常に有効化しておく ツールです。 これは「オーバーヘッドが小さいこと」と後ほど紹介する「イベントログのデータフロー」から成っている強みになりますが、それ故に JFR は 「何かトラブルが起きてデータが欲しくなったから計測開始」 ではなく、 「何かトラブルが起きたときに備えてずっと計測しておく」 という使い方ができます。 航空機のフライトレコーダーから名前を取っているようなのですが、まさしくその Java 版といったところです。 (もう少し身近なところで、 ドライブレコーダー の Java 版 と言った方が馴染みがあるかもしれません。) 取得情報はカスタマイズ可能 JFR は事前に定義された イベント という単位で情報を集めます。 *1 数あるイベントのうち、どれを取得するかは設定ファイルでカスタマイズすることができます。 独自の設定を作成することもできますし、テンプレートとして2種類の設定ファイルが JDK に同梱されていますので、それをそのまま使うこともできます。 default.jfc 本番環境での継続的な記録に向いたテンプレート設定 オーバーヘッドは1%ほど profile.jfc 検証向けのテンプレート設定 オーバーヘッドは2%ほど <設定ファイルの一部> <event name="jdk.ThreadAllocationStatistics"> <setting name="enabled">true</setting> <setting name="period">everyChunk</setting> </event> <event name="jdk.ClassLoadingStatistics"> <setting name="enabled">true</setting> <setting name="period">1000 ms</setting> </event> 設定ファイルは xml 形式になっています。 enabled 属性の true / false を切り替えることで取得イベントの選択ができます。 イベントログのデータフロー JFR が取得するイベントがログファイルとして出力されるまでの流れを紹介します。 登場人物の多くは JFR のオプションでサイズなどの調整が可能です。 *2 流れを把握し、環境に合わせて適切なオプションを指定することで、導入によるサーバリソースへの負荷を抑えることができます。 ◆ アーキテクチャ JFR ではイベントを「スレッドローカルバッファ」「グローバルバッファ」の2つのメモリ領域と「 リポジトリ 」と呼ばれるディスク領域で保管しています。 ◆データフロー ①拾ったイベントを「スレッドローカルバッファ」に格納  スレッドローカルバッファはスレッド単位の専用バッファのため、他のスレッドと競合することがありません。  初動でこのバッファを使うことで高速なイベント収集を実現しているそうです。 ②「スレッドローカルバッファ」から「グローバルバッファ」にコピー  スレッドローカルバッファは小さいためすぐに一杯になります。溢れた情報はグローバルバッファに移ります。  グローバルバッファはスレッド間で共有するプロセスのメモリで、循環バッファの形になっています。 ③「グローバルバッファ」から「 リポジトリ 」にファイル出力  グローバルバッファからも溢れた情報は リポジトリ というディスク領域に .part ファイルとして  出力されます。 ④part ファイルからチャンクダンプに変換  グローバルバッファから溢れた情報はどんどん .part ファイルに溜まっていきます。  こちらも一定のサイズたまると、 .jfr ファイルに変換されます。  (これをこの記事では チャンクダンプ と呼びます)  チャンクダンプは JFR のログファイルとして完全なバイナリになっており、JMC などのビューアーを使って  可視化することができます。  また、 リポジトリ 領域はログの 最長保管期間 や 最大保管サイズ をオプションで指定することができます。  ストレージを圧迫しすぎないような設定をおすすめします。 ⑤jcmd やプロセスの停止などによって JFR ファイルを出力  ③④で出力されるチャンクダンプはチャンクサイズごとに細切れになったファイルです。  jcmd によるダンプ出力の指示やプロセスの停止をトリガーとして、すべての記録が一つにまとまった  JFR ファイルを生成することができます。 *3  (これをこの記事では フルダンプ と呼ぶことにします)  フルダンプと一つひとつのチャンクダンプの中身は基本的に一緒ですが、フルダンプにはまだチャンクダンプに  吐き出されていなかったメモリ領域のイベントログも出力されます。 オーバーヘッドによる影響度検証 オーバーヘッドが小さく、故に常に有効化しておくことのできる JFR ですが、 そのオーバーヘッドが実際にアプリにどれぐらいの影響を与えるかを CPU・Javaヒープ使用量・プロセスが消費する最大メモリ量・ディスク吐き出し時のアプリ動作への影響・ダンプサイズ(ストレージへの影響) の5つの観点で検証してみました。 検証結果 少し長くなるので結果だけ先に述べておきます。 5つの観点ですが... 「 Java ヒープ使用量」への影響のみ要注意 その他は無視できるか、オプションで調整可能な範囲の影響 だと言える結果になりました。詳細をこれから記していきます。 CPU CPUへの影響は次の観点とメトリクスで見ていきます リクエスト処理中 の影響 → CPUが高稼働になっていた時間 を計測 待機中(リクエストを処理していない時) の影響 → CPU使用率 を計測 ◆計測方法 リク エス ト処理中にひたすらCPU負荷をかけるサンプルを用意してリク エス トを繰り返し送りました。 次の3パターンで試行し、2つのメトリクスを計測結果から抽出します。 JFR 無効 JFR 有効 / 本番向け設定ファイル(default.jfc)を使用 JFR 有効 / 検証向け設定ファイル(profile.jfc)を使用 ◆計測結果 各パターンの計測値を比較した結果です。 <リク エス ト処理中の影響(CPUが高稼働になっていた時間)> CPU( JVM ) 高稼働時間 CPU(マシン) 高稼働時間 ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 56.88 秒 - 56.90 秒 - JFR 有効(default.jfc) 57.15 秒 + 0.27 秒 [+ 0.47 %] 57.35 秒 + 0.47 秒 [+ 0.79 %] JFR 有効(profile.jfc) 57.21 秒 + 0.33 秒 [+ 0.58 %] 57.35 秒 + 0.47 秒 [+ 0.79 %] ※平均:20リク エス トの平均 ⇒ 増加率1%未満 の範囲でわずかに増加 という結果になりました。 <待機中の影響(CPU使用率)> CPU( JVM ) 使用率 CPU(マシン) 使用率 ※平均 JFR 無効との差 ※平均 JFR 無効との差 JFR 無効 0.986 % - 1.207 % - JFR 有効(default.jfc) 1.640 % + 0.654 % 1.738 % + 0.531 % JFR 有効(profile.jfc) 1.830 % + 0.844 % 1.869 % + 0.662 % ※平均:19期間の平均 ⇒ 増加量1%未満 の範囲でわずかに増加 という結果になりました。 Java ヒープ使用量 Java ヒープへの影響を見ていきます。 こちらも リクエスト処理中 と 待機中 の2つの観点で使用量の変化を確認しました。 ◆計測方法 リク エス ト処理中にメモリ負荷をかけ続けるサンプル と シンプルなDBアクセスのみをするサンプル の2種類のサンプルを用意して、それぞれでヒープの変化を計測しました。 また、こちらは正確な関係がわかっていないのですが、JFR を有効化していると初回のリク エス ト終了後にフル GC が走るという動きが見られました。 ( Java ヒープの New 領域や Old 領域に余白があってもフル GC が走っていました) そのため、 リクエスト処理中 待機中 それぞれで フルGC前 フルGC後 の2地点の計測を行いました。 こちらもCPUと同じく JFR の有無で結果を比較します。 ◆計測結果 こちらはいくつか条件を組み合わせて計測したのですが、その条件の違いや同一条件内でも試行ごとで計測値のブレが大きい結果となりました。 結果の一例を載せますが、こちらの結果では 一部(平均値で)20%近い増加率 も記録されています。 サンプルアプリの範囲での結果ですので実商材でも同じ結果になるとは言い切れませんが、 導入を検討する際の要注意項目となりました。 <リク エス ト処理中の影響( Java ヒープ使用量)> Java ヒープ使用量(フル GC 前) Java ヒープ使用量(フル GC 後) ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 247.8 MB - 32.9 MB - JFR 有効(default.jfc) 270.6 MB + 22.8 MB [+ 9.2 %] 36.8 MB + 3.9 MB [+ 11.9 %] JFR 有効(profile.jfc) 279.5 MB + 31.7 MB [+ 12.8 %] 34.5 MB + 1.6 MB [+4.9 %] ※同一条件で3回計測した平均 <待機中の影響( Java ヒープ使用量)> Java ヒープ使用量(フル GC 前) Java ヒープ使用量(フル GC 後) ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 205.2 MB - 30.6 MB - JFR 有効(default.jfc) 244.1 MB + 38.9 MB [+ 19.0 %] 34.7 MB + 4.1 MB [+ 13.4 %] JFR 有効(profile.jfc) 238.4 MB + 33.2 MB [+ 16.2 %] 32.5 MB + 1.8 MB [+5.9 %] ※同一条件で3回計測した平均 プロセスが消費する最大メモリ量 JFR がイベントログを溜める上で、プロセスとしてどこまでメモリを使用するか確認しました。 おそらくグローバルバッファのサイズに比例するだろうと仮定しつつも、際限なく食い潰される危険がないかを検証します。 計測メトリクスとしては「3日間アプリを稼働させた中で記録した RSS の最大値」を見ていきます。 検証でいろいろ試していく中で、 JFR のイベントログはCヒープ領域に保持されていそう だと理解したため、 RSS を取っています。( JVM に組み込まれているという特徴も相まってそう判断しています) ◆計測方法 サンプルアプリを立ち上げ、3日間ひたすらリク エス トを送り続けました。 ◆計測結果 ⇒ 想定通り、 グローバルバッファのサイズに準じて増加する という結果が得られました。 グローバルバッファのサイズは JVM 起動時のオプションなどで指定できます。 サイズの大きさとディスク吐出しの頻度は トレードオフ ですが、 環境に合わせてサイズを指定してあげれば影響は小さくできると言えます。 ディスク吐き出し時のアプリ動作への影響 JFRはそのデータフローの中で3種類のディスク吐出しを行います .part ファイル出力 チャンクダンプ出力 フルダンプ出力 それぞれのファイル出力時において、アプリに遅延が発生しないかを確認します。 リク エス トの処理時間をメトリクスとし、ファイル出力の有無による時間の差を見ます。 ◆計測方法 サンプルアプリに対して定期的にリク エス トを送ります。 .part ファイルとチャンクダンプは一定リク エス トを送れば自動で出力されますので、そのときのリク エス ト処理時間を計測します。 フルダンプは jcmd で出力指示を出して、そのときのリク エス ト処理時間を計測します。 ◆計測結果 ディスク吐出し時の処理時間と、平均処理時間を比較した結果です。 < .part ファイル出力時> 吐出しサイズ※ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 39.98 MB 8.809 秒 8.728 秒 - 0.081 秒 [- 0.92 %] 79.96 MB 9.090 秒 + 0.281 秒 [+ 3.19 %] 119.69 MB 8.917 秒 + 0.108 秒 [+ 1.23 %] ※partファイルの増加量 <チャンクダンプ出力時> 吐出しサイズ※ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 40.4 MB 8.883 秒 8.927 秒 + 0.044 秒 [+ 0.50 %] 80.7 MB 8.708 秒 - 0.175 秒 [- 1.97 %] 119.0 MB 8.917 秒 + 0.034 秒 [+ 0.38 %] ※(出力されたチャンクダンプのサイズ) - (直前のpartファイルのサイズ) <フルダンプ出力時> フルダンプサイズ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 111.7 MB 8.564 秒 9.645 秒 + 1.081 秒 [+ 12.6 %] 226 .3 MB 8.582 秒 9.912 秒 + 1.33 秒 [+ 15.5 %] 456.0 MB 8.677 秒 10.505 秒 + 1.828 秒 [+ 21.1 %] ⇒ 気にするべきは「フルダンプ出力時」になります。 フルダンプ出力時は、指示を出したタイミングで別プロセスが大量に立ち上がり、 アプリ( JVM )の使えるCPUが30%ほど減っていました。 結果、増加率 21.1 % という大きな数値につながっています。 とはいえ、フルダンプ出力は普段プロセスが動いているときに勝手に実行される機能でもありません。 フルダンプ出力の命令タイミングだけ気を付けることで回避可能 な影響ですのでクリティカルなものではないと考えています。 また、 .part ファイルやチャンクダンプ出力時の増加率は1桁%となっており、その影響は十分小さいと言えます。 ダンプサイズ JFR のダンプサイズがどれぐらいになるか計測します。 自社の商材ではログはアプリサーバからログサーバに転送しているのですが、 そのログサーバを圧迫するようなことがないかを確認します。 ◆計測方法 15時間アプリを稼働させ、次の観点の組み合わせで15時間後のダンプサイズを計測します。 設定ファイル default.jfc / profile.jfc リク エス ト回数 15時間で... 0リク エス ト / 約26,500 リク エス ト / 約53,000 リク エス ト / 約106,000 リク エス ト ◆計測結果 ⇒ ダンプサイズは リク エス ト回数に応じて微増 しました。 同じリク エス トを投げていたため同じイベントを大量に収集したことになりますが、同一イベントであれば JFR がいい感じに圧縮してくれていそうです。 計測結果から1日当たりのダンプサイズを試算してみると、 数万~数十万リク エス ト/日 規模のシステムであれば 、本番運用向けの default.jfc 設定でざっくり 300MB/日 程のサイズになると見込めます。 また、ダンプファイルは gzip や bzip2 で 圧縮をかければ3割のサイズにまで小さく できました。 実質 90MB/日 程です。 このサイズをどう捉えるかは各々の環境次第ではありますが、商材の持つ リク エス ト数 や アプリサーバの台数 なども加味して、場合によってはログローテートを見直すなどの対応が必要になりそうです。 まとめ JDK Flight Recorder の特徴と、オーバヘッドによるアプリへの影響を検証した結果を紹介しました。 アプリへの影響としては、 Java ヒープへの影響のみ要注意 だが、 その他は無視できるか、オプションで調整可能な範囲 だと言える結果になりました。 また、 機能を有効化するだけで使い始められる手軽さ と 収集できる情報の豊富さ は大きな魅力だと思っています。 小さくはじめられて大きなリターンを期待できます ので、この記事を読んでくださった方も是非 JFR を使ってみてください。 参考文献 OpenJDK での JDK Flight Recorder の使用 OpenJDK 11 | Red Hat Customer Portal フライト・レコーダを使用したパフォーマンス問題のトラブルシューティング JDK Flight Recorder入門 - Speaker Deck 目次 · 入門: JDK Flight Recoder 使ってみよう!JDK Flight Recorder OpenJDK 8uでJDK Flight Recorderの入門 - 赤帽エンジニアブログ GitHub - thegreystone/jmc-tutorial: A hands-on-lab/tutorial for learning JDK Mission Control 7+. *1 : 独自イベントを実装することもできます 参考: カスタムイベント API の定義と使用 *2 : オプションの参考: コマンドラインを使用した JDK Flight Recorder の設定 *3 : リポジトリ 領域の最長保管期間・最大保管サイズを越えて削除されたログは出力されません
こんにちは。 株式会社 ラク スで先行技術検証をしたり、ビジネス部門向けに技術情報を提供する取り組みを行っている「技術推進課」という部署に所属している鈴木( @moomooya )です。 ラク スの開発部ではこれまで社内で利用していなかった技術要素を自社の開発に適合するか検証し、ビジネス要求に対して迅速に応えられるようにそなえる 「技術推進プロジェクト」 というプロジェクトがあります。 このプロジェクトで「WEBアプリケーションのDockerコンテナ移行」にまつわる検証を行なったので、その報告を共有しようかと思います。 今回はコンテナ化そのものの話よりも、コンテナ化する際の環境や、対象のアプリケーション設計についてなど、周辺の話が多いです。 ちなみに中間報告時点で公開した記事はこちらになります。 tech-blog.rakus.co.jp 本検証での構成環境 既存のアプリケーション実行環境 アプリケーション概要 検証した環境 本検証で目指したこと、既存の課題 コンテナ化の際に検討および対応したこと 実行環境構築手順の整備 アプリケーション間の連携 システムコマンドの利用 CI Runnerの不足 ヘッドレスChromeを使ったE2Eテストでは--disable-dev-shm-usage 別リポジトリに格納されたE2Eテストコード アプリケーションサーバーとDBサーバーが分離していない前提のテストコード もしかしたらこんなことも必要かも ACME対応ローカルCA 使用しているgitコマンドパスとgitのオプション設定に注意 コンテナレジストリの用意 Dockerfileの管轄をどうするか まとめ 本検証での構成環境 既存のアプリケーション実行環境 Apache + PHP Postfix PostgreSQL これらは1台のWEBサーバーに相乗りする形でインストール アプリケーション概要 PHP で実装されたWEBアプリケーション メール関連アプリケーション 記録されている最古のコミットが2012年 開発開始自体は2001年ごろ=20年以上の長寿サービス 検証した環境 Apache + PHP + Postfix の相乗りコンテナ PostgreSQL コンテナ 今回はCIでテストさせたかったのでストレージもコンテナ内 Apache + PHP と Postfix をなんで相乗りさせているの? と思われるかもしれませんが理由は後述。 他にも Apache + PHP を php -fpm使って独立させないのか? という話も出ましたが、コンテナ化「+α」の部分であり、コンテナ化自体に必須ではないので今回はスコープ外としています。 まずはコンテナに乗せることが優先です。 本検証で目指したこと、既存の課題 先述の通り、コンテナ上で動作させることを最優先としています。いわゆるコンテナファースト的な設計への作り直しはスコープ外にしています 1 。 また、本番運用を視野に入れると考慮しなければならないことが増えてしまうので、今回はコンテナベースのCI、とりわけE2Eテストを可能にするまでです。「本番までコンテナに統一しないと意味ないのでは?」という声もありそうですが、既存の課題として以下のような物があり、開発環境のコンテナ化だけでも恩恵が充分あると判断しています 2 。 個人ごとに 仮想マシン ( VM )上に開発環境を作っているため環境差異によるトラブルが発生していた 新規参加メンバーの環境構築に手間がかかっていた 過去バージョン環境の再現が手間 チーム外メンバーがサポートする際に、動作環境の調達が面倒だった 最大の理由は1つ目ですね。ベースとなる VM イメージは共通のものを使用していますが、 VM は状態を保持してしまうので不意に差異が発生しがちです。こまめに再作成すればいいのかもしれませんが、面倒 3 なのでなかなかそうもいかず、という感じです。 コンテナ化の際に検討および対応したこと 実行環境構築手順の整備 最初に直面したのはOSインストールから始まる環境構築手順が見つからないことでした。 比較的最近開発され始めたサービスであれば大抵の場合は用意されていると思いますが、今回対象にしたアプリケーションではテンプレートとなる VM をコピーして構築する運用だったため、テンプレート VM を解析して手順作成を行うことになりました 4 。 コンテナ化する際には環境構築手順をDockerfileに書き起こす形で進めていくため、ゼロから構築することができる手順が必要です。 手順はアプリケーションの ソースコード レポジトリと一緒に管理しておく、少なくとも手順への参照情報が記載されているとコンテナ化にあたってスムーズに準備が進められるでしょう。手順はコード化されていると理想的ですが、 GitHub やGitLabなどには Wiki 機能もあるので、そこにまとめておいても良いと思います。 アプリケーション間の連携 1コンテナあたり1つの役割を持たせるのがコンテナとしてはセオリーだと思いますが、現状のアプリケーションは全ての機能が1つになっています。 このアプリケーションの分割を検討するにあたって、 PHP アプリケーションと ミドルウェア アプリケーションとの連携方法がコンテナを分割できるかどうかの分かれ目となりました。 このアプリケーションに含まれる主だった連携内容には以下のようなものがあります。 PHP と Postfix の連携 PHP から Postfix の設定ファイル更新や再読み込み Postfix からメール受信をトリガーにした PHP 実行 PHP と PostgreSQL の連携 PHP から PostgreSQL へのデータ入出力 これらのうち問題となったのは1つ目の PHP と Postfix の連携です。 これらは直接設定ファイルを編集していたり、 systemctl postfix reload といったコマンドで設定の再読み込みを行なっていたり、 php -f hogehoge.php といったコマンドで実行していたりしました。このようにOSを介した連携を行なっている部分は別コンテナにしづらく、HTTPなどの TCP/IP でやりとりするインタフェースを新たに実装する必要があります 5 。 一方で2つ目の PHP と PostgreSQL の連携は TCP/IP に則っているため、問題なく別コンテナにすることができました。コンテナイメージも PostgreSQLのオフィシャルイメージ を使うことができたので構築手順を簡略化できました。 システムコマンドの利用 既存のアプリケーションでは26種類260箇所のシステムコマンド呼び出しがありました。 もし、システムコマンドの呼び出し部分を置き換えるとすると改修に必要な 工数 の概算は以下のようになります。 修正コスト概算 1箇所あたりの改修コスト概算 2時間 影響調査: 1時間 修正:0.5時間 テスト:0.5時間 2時間 x 260箇所 = 520時間 = 3.25人月 互換性や依存関係などにより追加のコストが必要になる可能性あり これに対して、メリットとデメリットは以下のようになります。 得られるメリット 余計なコマンドがインストールされないためよりセキュアになる コンテナイメージが小さくなる 被るデメリット 3.25人月の修正コストがかかる 対応しなかった場合のデメリットは以下のようなものです。 コマンドがインストールされることでセキュリティ的に若干劣る 既存の環境では存在しており、使うコマンドだけインストールするため既存よりはセキュアになる コンテナイメージが大きくなる といっても Linux コマンド26種類の容量なのでそれほど大きな差にはならない 本来であればコンテナ内にインストールされるコマンドは最小限に抑えるべきでしょうが、修正コストと得られるメリット/デメリットを検討した結果、コンテナイメージをビルドする際に、必要なシステムコマンドをインストールする方法を選択しました。 CI Runnerの不足 CIにはGitLab CIを利用しましたが、Shared RunnerがなかったためCI Runnerを用意するところから準備しました。 CI Runner用のマシンリソースを用意できると良かったのですが、今回は検証期間中のみ使用するため各自のPCにCI Runnerのコンテナを起動し、Shared Runner(実際にはGroup Runner)として登録しました。 このあたりは普段のCI環境整備の一環として用意しておくと楽に進めることが出来ると思います。 ヘッドレス Chrome を使ったE2Eテストでは --disable-dev-shm-usage コンテナ環境では /dev/shm の容量が小さいため、デフォルトの状態だとクラッシュするようです。 この事象自体は chromium の Issueにも上がっている問題 で、回避策としては Chrome の起動オプションに --disable-dev-shm-usage を付与して /dev/shm の代わりに /tmp を使わせて回避するのが良いようです。 別 リポジトリ に格納されたE2Eテストコード 今回対象としたアプリケーションではE2Eテスト用のコードがメインのコードベースと別 リポジトリ で管理されていました。 今回、Docker Composeを利用していましたが、アプリケーションのDockerfileとテストコードの位置はそれぞれ 相対パス で指定する必要があります。今回は既存のルールとして特定のパスにそれぞれの リポジトリ を配置するルールだったので、 リポジトリ をまたいで 相対パス での指定が出来ました。 しかし実際には docker submodule などを利用して リポジトリ 内のパスとして参照できるようにするか、そもそも同一 リポジトリ 内で管理するようにしたほうが良いでしょう。 アプリケーションサーバ ーとDBサーバーが分離していない前提のテストコード アプリケーション本体はDBサーバー参照先も 環境変数 などで管理していることが多いと思いますが、テストコードはどちらもローカルホストである前提のコードが書かれていました 6 。 今回はDBサーバー参照先を地道に書き換えて対応しました。 もしかしたらこんなことも必要かも ACME 対応ローカルCA 弊社では各拠点ごとに外部ネットワークに接続する際、クライアント認証を行っています。そのためdnfコマンドやcomposerコマンドでパブリックなパッケージ レジストリ からダウンロードする際にはコンテナイメージ内にもクライアント証明書を持つ必要があります。 対処としてはコンテナイメージをビルドするタイミングでクライアント証明書を含める必要があります。しかも東京、大阪など拠点別に証明書は用意されているため、すべての証明書を含めないと手元で起動する際に通信出来ません。 しかし、クライアント証明書を含んだ状態のコンテナイメージをコンテナ レジストリ に格納するのはセキュリティ上好ましくありません。 今回はコンテナ レジストリ を使わなかった都合上、各検証メンバーの手元でコンテナビルドをしていたため必要にはなりませんでしたが、本格的に運用していくことを考えると、コンテナ起動時に Let's Encrypt のように自動で証明書を取得できるようなローカル 認証局 を用意するのが良さそうに思えます。 今回は検証スコープ外としましたが、 ACME という プロトコル に対応した 認証局 を用意することで実現できそうです。 使用しているgitコマンドパスとgitのオプション設定に注意 コンテナというよりもgitの話なのですが、今回の検証で Windows + WSL2環境でDockerを動かしていた環境がありました。この環境上でcloneしたコードで docker-compose build がエラーになるという問題が発生しました。 結論としてはgitのautocrlf設定が期待どおりに設定されていなくて、docker-compose.ymlの改行コードが変換されてしまっていた、という話だったのですがこれが起きた経緯が以下のようなものでした。 WSL2上のgitはautocrlf=falseとなっており、改行コードが変換されることはないはずだった PhpStormでgit cloneしていたが、PhpStormは Windows 上のgitを参照していた Windows 上のgitはデフォルト設定のautocrlf=trueでインストールされていた わかってしまえば単純なミスだったのですが、私自身は Windows 環境においてはWSL2上のシェルから コマンドライン でgitを使っていて盲点だったので記録しておきます。 コンテナ レジストリ の用意 弊社では一部を除いてコンテナを用いた開発を行われていないので社内から使えるプライベートなコンテナ レジストリ がありませんでした。 今回の検証中ではビルド→E2Eテスト、の間のコンテナイメージの受け渡しをマルチステージビルドで賄ったためなんとか使わずにすみましたが、実際の開発環境で運用する際にはコンテナイメージを共有したいのでコンテナ レジストリ が欲しくなると思います。 Dockerfileの管轄をどうするか アプリケーション開発チームとインフラチームが分かれている場合、Dockerfileやdocker-compose. yaml の更新をどちらのチームがどうやるかという問題が発生します。 ここでいうインフラチームは ミドルウェア 周りを担当しています。 両チームが同じファイルを更新する インフラチームが作成したコンテナイメージをベースイメージとして、アプリケーション開発チームが仕上げる という方法が考えられると思います。今回はインフラチームと一緒の検討を行ったわけではないので推測となりますが、以下理由により同じファイルを扱うのが良いと考えています。 別ファイルにすると取り込み漏れが発生しそう 例えば次期バージョンで必ず入れないといけない ミドルウェア のセキュリティアップデートがリリースされない、など FROM image:latest のように必ず最新版を参照するようにすれば必ず反映されるが…… リリース直前にベースイメージが変わってしまう可能性がある テストがやり直しになる ベースイメージはやはりバージョン指定で使いたい 同一ファイルの場合、更新制御にPull Requestなどの バージョン管理システム の機能が利用できる 別ファイルでも利用はできるが共通管理じゃないとやりにくそう まとめ コンテナはできるなら導入したほうがいい 7 。 ただし巨大プロジェクトを立ち上げて移行する必要はあまりなく、コンテナ化に向けて障害となる課題を一つずつ解消していく形で進めることができそうです。 そしてそれら一つ一つも 開発プロセス や設計の改善となっているため、無駄がないという印象を得られました。 直接的に売上が増えるわけでもないので「コンテナ化」だけでそんな予算取れないです。あと一度の改修は出来るだけ小さくしたいし、コンテナ化してCI回しやすくなれば改修の安全性も上がるので後から追加で直していくのが無難だと思っています。 ↩ もちろん段階的には本番環境でのコンテナ動作も目指したいとは思っていますが。風呂敷を広げすぎると計画が頓挫していつまで経っても改善出来なかったりするので、分割できる問題はできるだけ小さくしたいです。 ↩ 面倒というのはサボるとかそういう話ではなく、手間がかかって非効率という意味合いです。面倒な作業を面倒と認識することは改善する上で重要です。エンジニアの三大美徳である怠惰にも通ずる話ですね。 ↩ 「テンプレートとなる VM を構築する手順がないのはおかしいよね」ということで探していたら、テンプレートの解析が終わった頃に参考資料が見つかった。 ↩ むりやり ssh で実行することも可能だとは思いますが、今後の柔軟性を考えると一般的なRESTなどの TCP/IP 通信で連携できるようにしたほうが良いでしょう。 ↩ テストコードはなるべく静的なコードにすることが多いと思うので、意外とありがちなのではないかと思っています。 ↩ コンテナでの動作を前提としたツールが増えているため、今後の 開発プロセス を考えると必要になると思われる。 ↩
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ 今回の記事はOPcacheのpreloadが出来るようになろう! という内容です。 尚、OPcacheのpreloadの基本設定とかについては以下の記事を参考にしてください。 tech-blog.rakus.co.jp 今回は上記記事では話していなかったpreloadのよくありそうな失敗話になります。 PHPerKaigi2023では語れなかった部分だったので是非見てもらえればと思います。 そして preloadとrequire_onceとかとの関係性について理解を深め て、更なる改善につなげてもらえればと思います! では早速はじめていきましょう! 失敗談:preloadしても変化がない! 解決策 説明用のファイルはこちら 試してみよう! まとめ 失敗談:preloadしても変化がない! 前回も張った画像ですが、まずこの図ですね。 preloadの説明 この図の通りでいうなら、 「お?preload使えば動的にロードなくなるんだ!」 ってなるのですが、実際どうなるか? 手順は以下です。 preloadの設定解除して count(get_included_files()) を出力 apache を再起動 preloadの設定して count(get_included_files()) を出力 この数の差がpreloadで事前ロードが成功した数になります。 私のとある開発環境では300ファイルのinclude数が150ファイルぐらいまで減らす事ができました。 「そのpreload出来なかった150ファイルは一体なんぞや!」 これを今日は深掘っていきますよ! 解決策 説明用のファイルはこちら 今回のこの問題を説明する用に簡単な PHP ファイルを用意しました。皆さんも手元で試してもらうと良いかな! <?php # index.php require_once ( "autoload.php" ) ; print_r ( get_included_files ()) ; echo "loaded : index. \n " ; $ a = new A () ; $ a -> call () ; print_r ( get_included_files ()) ; <?php # autoload.php spl_autoload_register ( function ( $ className ) { echo "spl_autoload: $ className . \n " ; require_once ( $ className . ".php" ) ; } ) ; <?php # A.php class A extends AP { public static string $ BName = B :: name; public function call () { echo "call : " . __class__ . ", B#name : " . self ::$ BName . " \n " ; } } <?php # AP.php class AP { public function call () { echo "call : " . __class__ . " \n " ; } } <?php # B.php class B { public const name = "B#name" ; } <?php # preload.php $ root_path = dirname ( __FILE__ ) . "/" ; # 以下例えばのpreload 一旦コメントアウトしておきますね。 #opcache_compile_file($root_path."AP.php"); #opcache_compile_file($root_path."A.php"); #opcache_compile_file($root_path."B.php"); #opcache_compile_file($root_path."index.php"); #opcache_compile_file($root_path."autoload.php"); echo ( "Preloading Success \n " ) ; 試してみよう! まずはpreload. php を php .iniに設定しないでやるとどうなるでしょうか? $ php index.php Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. spl_autoload: A. spl_autoload: AP. spl_autoload: B. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php [ 2 ] = > /usr/ local /application/works/A.php [ 3 ] = > /usr/ local /application/works/AP.php [ 4 ] = > /usr/ local /application/works/B.php ) 実行時に index.php がロードされて、require_onceで autoload.php がロードされて、 各クラスが必要になったタイミングでspl_autoloadがされていってるんだな! っていうのがよくわかりますね。 では、 preload.php で A.php をpreloadするとどうなるでしょうか? $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. spl_autoload: A. spl_autoload: AP. spl_autoload: B. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php [ 2 ] = > /usr/ local /application/works/A.php [ 3 ] = > /usr/ local /application/works/AP.php [ 4 ] = > /usr/ local /application/works/B.php ) 変わってないじゃないか!!!!ログをみてみると... $ cat /var/ log /messages | tail -n1 Mar 28 20 : 19 : 18 auto7-dev045 php: PHP Warning: Can 't preload unlinked class A: Unknown parent AP in /usr/local/application/works/A.php on line 3 なるほど、 preloadというのは親クラスとかも一緒にpreloadしてあげないといけないんですね! よし、では AP.php もpreloadして再度トライ! $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. spl_autoload: A. spl_autoload: B. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php [ 2 ] = > /usr/ local /application/works/A.php [ 3 ] = > /usr/ local /application/works/B.php ) おーい!もっかいログを見てみましょう! $ cat /var/ log /messages | tail -n1 Mar 28 20 : 27 : 23 auto7-dev045 php: PHP Warning: Can 't preload class A with unresolved initializer for static property $BName in /usr/local/application/works/A.php on line 3 なになに、static propertyでつかってる$BNAMEがわかりませんと... static propertyとかで使われてる値も一緒にpreloadしないとダメ! って事ですね! よし、じゃ B.php もpreloadしましょう!再度トライだ! $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) 成功ですね!!!あとは autoload.php だけか!よし、これもpreloadしましょう! $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) なぜ!!ログを見てみましょう!ってログも何も出てないよ!なぜか?答えは preloadしていてもrequire_onceとかしちゃうと結局ロードされちゃいます!! ですので、index. php のrequire_onceを削ってみましょう! こんな感じです。 <?php # index.php print_r ( get_included_files ()) ; echo "loaded : index. \n " ; $ a = new A () ; $ a -> call () ; print_r ( get_included_files ()) ; 結果は・・・・? $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php ) loaded : index. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php ) 大成功!ついにincludesファイルが0個になりました!! まとめ はい、まとめです。preloadって事前ロードしてくれる機能ではあるのですが、内部で使われているクラスに漏れがあると、そのクラス自体のpreloadも出来ない! そして、require_onceなどしているものは結局preloadされてしまう!という事なんですね。 皆さんもこれを参考にしながらpreloadの設定頑張ってくださーい!!
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ なんと嬉しい事に PHPerKaigi 2023 での登壇が決まりました☆ この記事出る頃には登壇終わってるけど!(汗) 題材は「 パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 」 https://fortee.jp/phperkaigi-2023/proposal/4a67cc68-83f0-492d-86ca-54304fc256c8 本セッションではパフォーマンス改善の具体的な手法まで深掘りせずに、広く浅く触れていこうかなと考えていますので、是非マネージャーなんかもご視聴頂ければと思います! という事で今回このブログでは逆に技術に深掘りした内容を話しちゃおうと思います! 今回は PHP のパフォーマンスチューニングの1つにあるOPcacheの中でもpreloadという機能に着目して説明していきたいと思います! 結論からいうと、 OPcacheのpreloadを使う事で php 処理が相当速くなる可能性があります!! 是非、皆さまの作成しているサイトでも採用を検討してくださいませっ! OPcacheとは? preloadとはどんな機能なのか? preloadの設定をやってみよう! preload.phpの中身 どのくらい動的にインクルードされているのか?見てみよう。 preloadの検証結果 さいごに 参考文献 OPcacheとは? 私たちが普段書いている PHP 言語が コンパイル されたコードをOPcode(オペコード)と呼ばれており、それをメ モリー 上に保持(キャッシュ)する事で、 PHP の処理を高速化しよう!という機能。 これまでもそういう取り組みはあったんですが、PHP5.5からはOPCacheが標準機能として採用され今に至っています。 preloadとはどんな機能なのか? preloadはOPcacheの更に一部の機能です。先ほどはOPcodeをメモリ上にキャッシュする所までやりましたが、 「じゃもぉこれ事前にloadしておいたらautoloadとか不要で更に早いんじゃないの?」という機能になります。 事前にload = pre-load うん、そのままですね! 絵で描くとこんな感じですね!(PHPerKaigi2023の登壇資料より抜粋) OPcacheの処理(PHPerKaigi2023の登壇資料から抜粋) preloadの設定をやってみよう! preloadが何か?がわかったので、具体的な設定の例を以下に記載しておきます。 [opcache] zend_extension=/path/to/opcache/opcache.so opcache. enable = 1 opcache.preload=/path/to/preload/preload.php opcache.preload_user=root opcache.jit_buffer_size = 256M opcache.memory_consumption= 256 opcache.interned_strings_buffer= 8 opcache.max_accelerated_files= 100000 opcache.revalidate_freq= 0 opcache.enable_cli= 1 opcache.validate_timestamps= 0 opcacheのオプションの細かい意味は 公式サイト にまとめられていますので、こちらを参考にしてください。 preload. php の中身 今回私の方では以下のようにしています。 <?php function _preload ( $ preload , string $ pattern = "/\.php$/" , array $ ignore = []) { if ( is_array ( $ preload )) { foreach ( $ preload as $ path ) { _preload ( $ path , $ pattern , $ ignore ) ; } } else if ( is_string ( $ preload )) { $ path = $ preload ; if ( ! in_array ( $ path , $ ignore )) { if ( is_dir ( $ path )) { if ( $ dh = opendir ( $ path )) { while (( $ file = readdir ( $ dh )) !== false ) { if ( $ file !== "." && $ file !== ".." ) { _preload ( $ path . "/" . $ file , $ pattern , $ ignore ) ; } } closedir ( $ dh ) ; } } else if ( is_file ( $ path ) && preg_match ( $ pattern , $ path )) { if ( ! opcache_compile_file ( $ path )) { trigger_error ( "Preloading Failed" , E_USER_ERROR ) ; } } } else { echo "IGNORE: $ path \n " ; } } } _preload ([ "/var/www/application" ] , "/\.php$/" , [ "/var/local/application/ignore/hoge.php]); _preloadメソッドは指定フォルダにある、指定拡張子のファイルを 再帰 的にpreload対象にするメソッドとして定義しています。 3つ目の引数でその中で除外したいファイルを指定できるようにしています。 正直ベストアンサーだとは言えませんが、とりあえず効果を見たいのであれば 普段 spl_autoload_register() を使って読み込んでいるファイルを網羅的に指定すればOKです! どのくらい動的にインクルードされているのか?見てみよう。 さて、設定が 終わった人 はきっと不安な事でしょう。 「動いてるけど、効果出てるの...?」 実際、1ページのロード時間でいうと体感できるかは難しいかと思います。 よって設定が有効が働いているかの確認は以下が宜しいかと思います。 preloadの設定解除して count(get_included_files()) を出力 apache を再起動 preloadの設定して count(get_included_files()) を出力 この数の差がpreloadで事前ロードが成功した数になります。 ※上記は動的にロードされたファイル数をカウントしています。なぜプレロード(事前ロード)されなかったのか?については長くなるので次のブログに書きますね! preloadの検証結果 試しに、私の担当する商材に対して50画面x4回=200回のアクセスを計測してみたところ、 約2.8%の改善効果がありました。 この数字はあくまで参考値だと思って下さい。というのは、今回早くなったのはファイルを事前ロードしておく事による効果です。 つまり、事前ロードにかかってる時間が占める割合が元々大きければ大きいほど改善するという事になります。 プロファイル結果(PHPerKaigi2023登壇資料抜粋) 上記は2023年3月24日のPHPerKaigi2023の登壇資料の一部です。私の担当する商材をプロファイリングした結果になるのですが、遅い原因がDBであればそこを改善しない事には改善しません。 ですが、実はこの登壇資料中では語っていませんが、上から2番目にあるSelf:5.50% となっている場所は spl_autoload_register() の処理にかかった時間の割合を指しています。 つまり この場合は最大5.5%の改善が見込める という事ですね! さいごに 皆さまも、是非ご担当されているサービスが遅い原因が何かを計測した後には、色んな改善方法があるんだなぁという一環でお試し頂ければと思います! 参考文献 Symfony DOCS - Performance Amazon Linux 2のPHP 7.4環境に、OPcacheを導入する OPcache のインストール手順 PHP7.4のpreloadいれたらLaravelは早くなるのだろうかと思って検証した @mpywさんの個人的なTweet ソースコードから理解するPreloadとJITの話 PHP: OPcache - Manual
こんにちは。 技術広報の syoneshin です。 明日、 PHPerKaigi2023 が開催されます。 今回当社はシルバースポンサーとして協賛させていただきました。 PHPerKaigi2023の概要は以下 開催:2023年3月23日(木)〜 3月25日(土) 場所: 練馬区 立区民・産業プラザ Coconeriホール および ニコニコ生放送 対象: PHP エンジニアおよびWeb技術のエンジニア 主催:PHPerKaigi 2023 実行委員会 参加申込: チケット購入サイト 公式サイト: PHPerKaigi2023 当社メンバー登壇情報 「楽楽販売」の開発に関わっているリードエンジニアの 前田 がレギュラー トーク で採択されました。 トーク タイトル「パフォーマンスを改善せよ!大規模システム改修の仕事の進め方」 登壇日時:3/24(金)16:25~ @Track A 詳細は以下 fortee.jp 他にも以下6名が3/24(金)、3/25(土)でLT登壇します。※以下は詳細情報 不幸を呼び寄せる命名の数々 ~君はそもそも何をされてる方なの?~ by 山村 光平 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp stdClassって一体何者なんだ?! by 寺西 帝乃 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp 特徴、魅力を知って、各PHPフレームワークを使いこなそう! by 浅野 仁志 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp 【実録】「PHP_CodeSniffer」で始める快適コードレビューライフ by 森下 繁喜 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp PHPマジックメソッドクイズ! by 加納悠史 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp フレームワークが存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 by 廣部 知生 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp 当日会場にお越しの方は、ぜひお気軽にお声がけいただけると嬉しいです! PHPer トーク ン 当社プロダクトの一部は PHP で開発しており、開発から得られた知見を PHP のコミュニティへ還元することで、技術発展に貢献したいと考えております! 毎月ブログを書いたり、Connpassで「PHPTechCafe」というイベントを主催したりしておりますので、ぜひ定期的にチェックいただけると嬉しいです! PHP 関連ブログ tech-blog.rakus.co.jp Connpassイベント rakus.connpass.com ここまでお読みいただきありがとうございます! PHPerKaigi2023のPHPer トーク ンは以下です #PHPTechCafe 皆さん、当日は盛上っていきましょう!!
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年8月のイベントでは「 PHP8.2の新機能 」について語り合いました。 弊社のメンバーが事前にまとめてきたPHP8.2の新機能に関する情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 PHP8.2 新機能について Deprecate dynamic properties 「AllowDynamicProperties」アトリビュートによる救済措置 Readonly classes Constants in Traits 従来のトレイトの問題点 Deprecate ${} string interpolation ${}表記が非推奨になる理由 パターン1: 配列 パターン2: オブジェクトのプロパティ パターン3: メソッドコール Deprecate partially supported callable MySQLi Execute Query mysqli_execute_queryとは 使い方 Fetch properties of enums in count expressions Allow null and false as stand-alone types Add true type Random Extension 5.x Disjunctive Normal Form Types その他のRFC Remove support for libmysql from mysqli Make the iterator _*() family accept all iterables Redacting parameters in back traces Deprecate and remove utf8_decode() and utf8_encode() Locale-independent case conversion まとめ PHP8.2 新機能について PHP8.2の新機能は弊社のメンバーが事前にShowNoteにまとめています。 今回のイベントではこのノートに沿って新機能をみていきました。 hackmd.io Deprecate dynamic properties 未定義のプロパティに値を代入した時の動作が変更され、動的プロパティが実質使えなくなります。 PHP8.1まで: プロパティが生成される PHP8.2: Warningが発生するが、プロパティが生成される。 PHP9.0: 例外が発生する。 以下のコードを例では、Userというクラスで未定義のプロパティnane(nameの typo )に値を代入しようとしているため、PHP8.2ではWarningが発生するようになります。 <?php class User { public $ name ; } $ user = new User; // Assigns declared property User::$name. $ user -> name = "foo" ; // Oops, a typo: $ user -> nane = "foo" ; // PHP <= 8.1: Silently creates dynamic $user->nane property. // PHP 8.2: Raises deprecation warning, still creates dynamic property. // PHP 9.0: Throws Error exception. この新機能については「タイポなどによって、意図しないプロパティが生成されることを防げるため、良い変更点」と紹介されましたが、その一方で「影響の大きい変更であるため、バージョンアップの対応が辛そう」というバージョンアップの大変さを嘆く弊社エンジニアの意見もありました。 また、「trait経由だと生やせる」「一回serializeしてunserializeするときに生やせる」など抜け穴が見つかっていることも話題に上がっていました。 「AllowDynamicProperties」 アトリビュート による救済措置 昔のプロジェクトでは、未定義のプロパティを意図的に生やすということを行うこともあったようです。 このようなレガシープロジェクトへの救済措置として #[AllowDynamicProperties] という アトリビュート を付ければとりあえず例外を回避できます。 <?php #[AllowDynamicProperties] class User () {} $ user = new User () ; $ user -> foo = 'bar' ; 参加者からはこの救済措置について、以下の意見があがっていました。 PHP8.0の時は影響の大きい変更がありましたが、そんなに救済措置がなく「用意すべき」という声があがっていたので、こういう救済措置を入れるのは優しいなと思う。 PHP を長くご愛好している人ほど辛い思いをするっていうのは本意ではないと思うので、ちょっとは考えてもらわないと往年の PHP プロジェクトは辛いと思う。 結局これを色んなクラスにつけていく未来しか見えないので、中々の苦行が待っていそう Readonly classes PHP8.1では上書きできないプロパティであるReadonly propertyが追加されましたが、今回のPHP8.2ではReadonly classesが追加されます。 Read Propertyについては過去の PHP TechCafe「PHP8.1をもっと語り合う」でも触れられています。 ※参考 tech-blog.rakus.co.jp Readonly classesではクラスを readonly で宣言することによって、そのクラスのプロパティ全てがreadonly機能を持つことになります。 動的プロパティも当然できないため、先ほど話題に上がっていた #[AllowDynamicProperties] アトリビュート をつけてもエラーになります。 「Deprecate dynamic properties」についてはエンジニアの中で意見が割れていたようですが、この機能については反対意見があまりなかったようで、 イベント参加者からも「不変な値オブジェクトとして利用できるんじゃないかなと期待している。」といった前向きな意見がありました。 Constants in Traits PHP には、コードを再利用するための トレイト という仕組みがあります。 ※参考 PHP: トレイト - Manual PHP8.2ではこのトレイトに、定数を定義できるようになります。 従来のトレイトの問題点 これまではトレイトに定数を直接定義することができず、クラスかインターフェースに定義した定数を利用するしかありませんでした。 しかし、この2つの方法にはそれぞれ難点があります。 クラスに定数を定義する場合 トレイトを利用するための定数定義がトレイト自体によって提供されていない。 インターフェースに定数を定義する場合 インターフェース側で定義していることは実装上自然であるが、トレイトを利用するクラスがインターフェースを実装していないといけない。 以上の問題点もあったため、今回のようにトレイトで定数を定義する事ができるようになることで、モジュールとしてのトレイトの完全性が向上するということで提案されました。 参加者からも「本当に使いやすさだけが向上した素敵なアップデートだと思う」という喜びの声があがっていました。 Deprecate ${} string interpolation 文字列で変数展開する際の ${} 表記の挙動が非推奨になります。 <?php $ foo = "bar" ; $ bar = "foo" ; var_dump ( " $ foo " ) ; // OK var_dump ( " { $ foo } " ) ; // OK var_dump ( " ${ foo } " ) ; // 非推奨: 文字列での ${} の使用は非推奨 var_dump ( " ${$foo} " ) ; // 非推奨: 文字列での ${} (可変変数) の使用は非推奨 PHP のヒアドキュメントなどでも、変数の中身を出力する時に${}が使えましたが、PHP8.2からは非推奨になるようです。 ${} 表記が非推奨になる理由 ${} 表記が非推奨になる理由については、挙動がよく分からないというものがあります。 例えば、挙動が分かりにくくなるパターンとして以下のようなパターンが挙げられます。 パターン1: 配列 <?php $ foo = [ 'bar' => 'bar' ] ; var_dump ( " $ foo [ bar ] " ) ; var_dump ( " { $ foo [ 'bar' ]} " ) ; var_dump ( " ${ foo [ 'bar' ]} " ) ; // すべて "bar" が出力されるが最後の挙動も "bar" になることが理解しづらい // しかも、可変変数は利用できない こちらの例では、$fooという変数の中にbarという 連想配列 がある状態です。 var_dump("$foo[bar]"); や var_dump("{$foo[‘bar’]}"); というのは問題ないのですが、 var_dump("${foo[‘bar’]}"); とするとどういう挙動になるのか想像しにくくなります。 さらにこのパターンの場合、可変変数が利用できないという点でも問題になります。 パターン2: オブジェクトのプロパティ <?php $ foo = ( object )[ 'bar' => 'bar' ] ; var_dump ( " $ foo -> bar " ) ; var_dump ( " { $ foo -> bar } " ) ; // このパターンでは ${} パターンが利用できない オブジェクトのプロパティでは、そもそも${}の表記が使えないです。 こちらも${}の挙動の分かりにくさの一因になっています。 パターン3: メソッドコール <?php class Foo { public function bar () { return 'bar' ; } } $ foo = new Foo () ; var_dump ( " { $ foo -> bar() } " ) ; // メソッドコールで許容されているのは上記のみ メソッドコールでも {$foo} 表記しか使えません。 以上のように一番複雑なのは${}というパターンであり、これがあると分かりにくいということからPHP8.2からは非推奨、PHP9.0からはエラーになるようです。 こちらも影響の大きい仕様変更に思えますが、「検出が簡単」「それぞれのパターンの直し方が RFC で解説されているので、それを参考に書き直したら問題ない」という観点から「バージョンアップ作業はそこまで大変ではないのでは?」という結論に落ち着いていました。 ※参考: RFC Deprecate ${} string interpolation wiki.php.net Deprecate partially supported callable call_user_func($callable_func)では実行できるが、$callable_func()では実行できない以下のような構文が将来的に廃止されます。 <?php "self::method" "parent::method" "static::method" [ "self" , "method" ] [ "parent" , "method" ] [ "static" , "method" ] [ "Foo" , "Bar::method" ] [ new Foo, "Bar::method" ] この機能についてもPHP8.2で非推奨になり、PHP9.0で廃止されます。 PHP8.1でFirst-class callable syntaxという機能が追加され、今回の一部のcallableの廃止はそれに付随しているようです。 ※参考: RFC First-class callable syntax wiki.php.net こちらの機能の廃止については 「変な呼ばれ方や変な構文をなくしていきたいって狙いですね。」「さっきの ${} もそうですけど、統一されていないようなものはどんどん消していきましょうって事ですね。」と参加者の間で考察されていました。 MySQLi Execute Query MySQLiに新しい関数mysqli_execute_queryが追加されます。 mysqli_execute_queryとは 下記3つの関数をまとめた関数です。 mysqli_prepare() mysqli_execute() mysqli_stmt_get_result() 使い方 PHP8.1以前 <?php $ statement = $ db -> prepare ( 'SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)' ) ; $ statement -> execute ([ $ name , $ type1 , $ type2 ]) ; foreach ( $ statement -> get_result () as $ row ) { print_r ( $ row ) ; } PHP8.2以降 <?php foreach ( $ db -> execute_query ( 'SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)' , [ $ name , $ type1 , $ type2 ]) as $ row ) { print_r ( $ row ) ; } こちらの関数追加については投票結果が全会一致だったようです。 参加者からも以下のような賛成の声があがっていました。 確かにめんどくさいなと思っていたので、「うん、確かに」としか言いようがなかった。 Prepairとか使ってきれいに書かないと安全性が担保できないくらいなら使いやすくしようと、実に正しい解決法ですね。 Fetch properties of enums in count expressions PHP8.1で複数の定数をまとめる Enums型 が追加されました。 ※参考 PHP: 列挙型 / Enum - Manual しかし、PHP8.1の enum のcaseはオブジェクトである為、 連想配列 のキーに使うことができませんでした。 そこでPHP8.2では、 -> を使って定数式の中で enum のプロパティを取得できるようになりました。 <?php enum A : string { case B = 'B' ; // 現在コレはダメ const C = [ self :: B -> value => self :: B ] ; } enum E : string { case Foo = 'foo' ; } // 'Foo'が取れる const C = E :: Foo -> name ; ライブラリの作者さんが安全なコードを書くのにほしいという需要があったようですが、「いまいち使いどころが分からない」という意見もありました。 Allow null and false as stand-alone types 元々UNION型でのみ、利用可能だった False型 と Null型 が型宣言が許される位置全てにおいて利用可能になります。 関数がエラーであることを示すために、返り値としてfalseを返す場合があります。 これを表すためにFalse型というものが作られましたが、この型がどこでも使えるようになります。 この変更により「静的解析で便利になるのでは?」という期待の声があがっているようです。 Add true type 先ほど紹介したNull型とFalse型が入った後、じゃあ True型 もあっていいのでは? という流れからTrue型が追加されることになりました。 参加者からは「True型ってそんなに使うことあるんですか?」という声もあがりましたが、別の参加者から RFC に紹介されているサンプルコードをもとに ユースケース が説明されました。 True型のサンプルコード( PHP: rfc:true-type より抜粋) <?php class User { function isAdmin () : bool } class Admin extends User { function isAdmin () : true { return true ; } } UserクラスのisAdmin()の返り値はboolで型指定されていますが、Userクラスを継承したAdminクラスは必ずtrueを返す仕様になっています。 このような場合に返り値にTrue型を指定することで、静的解析でより有用なチェックができるようになるというケースがあるようです。 この説明を受けるとTrue型の有用性について疑問を抱いていた参加者も納得している様子でした。 Random Extension 5.x PHP で乱数を使用する関数の実装に問題があることが以前から指摘されていました。 具体的には メルセンヌ・ツイスター (疑似乱数ジェネレーター)の実装が壊れている メルセンヌ・ツイスター の状態は PHP のグローバル領域に暗黙的に格納される。そのため外部ライブラリで乱数が乱れたりといった問題が発生する。 shuffle(), str_shuffle(), array_rand(),random_int()といった組み込み関数では メルセンヌ・ツイスター が乱数ソースとして使用されるため、暗号的に安全な乱数が必要な場合は危険。 PHP での乱数の実装は、歴史的な理由から標準モジュール内に散らばっている。 などが挙げられます。 そのため、まざまなランダム化メソッドを提供する単一のクラス、 Random\Randomizer クラスが追加されます。 この RFC が出された理由は、 RFC を出された方のスライドを見ると全て分かると以下のスライドが紹介されていました。 speakerdeck.com こちらのスライドに目を通した参加者からは以下のような意見があがっていました。 外部の影響を受けて乱数の値が変わるのは、確かに乱数とは言えない。 そもそも乱数生成のPOSTが高かったみたいですね。なのできちんとライブラリを使って速度も早く安定したRandomを作りたいと。素晴らしい話だと思います。 Disjunctive Normal Form Types プロパティやパラメータに対して、論理式の形式で複雑な型指定をすることができるようになります。 RFC では A | (B&D) | (B&W) | null といった細かい例もあり、かなり細かい型指定ができるようになるようです。 ゲームのパラメータなどで、クラスに対して型を定義し、これとこれは受け取るけどこれは受け取らない、というように細かい制御を行うことがあるという事例が紹介されていましたが、 参加者からは「 PHP でそこまで型を使いこなすようなことをやるのか?」「何かに使えそうな気はするけどパッと例は出てこない」など疑問の声もあがっていました。 その他の RFC 上記で紹介した RFC 以外にも、「 PHP TechCafe」では以下の新機能や変更点について触れられていました。 Remove support for libmysql from mysqli MySQL のモジュールはmysqlndとlibmysqlの2種類が存在するため、このうちlibmysqlを削除するという内容です。 PHP5.4以降はmysqlndがデフォルトとなっており、libmysqlを選ぶ利点がほぼないことから全会一致でlibmysqlの削除が決まったようです。 Make the iterator _*() family accept all iterables foreachに渡せる反復可能なオブジェクトである iterator ですが、PHP8.2では iterator _to_array()と iterator _count()にiterable型の変数を渡せるようなります。 Redacting parameters in back traces スタックトレース に引数を出力しないようにする アトリビュート #[\SensitiveParameter] が追加されます。 参加者からは「DBの接続情報などが画面に表示されることによる情報流出を防ぐことができるのでは?」という期待の声があがっていました。 Deprecate and remove utf8_decode() and utf8_encode() utf8_decode()とutf8_encode()が削除されます。 この関数は、任意の文字列を UTF-8 に エンコード デコードできるかと思いきや、変換相手はLatin 1固定であり、誤解を招く関数名であることから削除されたようです。 Locale-independent case conversion ロケール の影響を受けていたstrtolowerなどの関数が、PHP8.2から ロケール の影響を受けないようになります。 そもそも従来のstrtolowerなどの関数が ロケール の影響を受けることに対して驚きの声があがっていました。 まとめ 今回はPHP8.2の新機能について、イベント参加者の生の声を交えてまとめてみましたがいかがでしたでしょうか? イベントでは追加される新機能の内容だけでなく、採用が決まるまでの裏話なども語られており、有意義なTech Cafeであったと思います。 PHP8.3についても導入される機能が続々と決まってきているので今後もどのような機能が PHP に追加されるのか注目していきたいです! 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
技術広報の syoneshin です。 いつも ラク スエンジニアブログをお読みいただき、ありがとうございます! 先日(2022/12/7)開催の ラク スMeetup。 今回は、 インフラ・CI/CD・自動化 をテーマに開催! 業務課題の カイゼン /効率化を目指すインフラエンジニアが登壇し取り組みをご紹介しました。 なお、本イベントは以下のような方にオススメとなっております。 ・ ラク スのインフラに関する取り組みについて知りたい方 ・ SaaS 企業のCI/CD, GitOps等の最新事例が気になる方 ・ レガシーシステム の改善事例を聞きたい方 ・自動化システムの継続運用にお悩みの方 ・システムを安全に運用するための施策を知りたい方 ・ ラク スのプロダクト、組織に興味がある方 ・ SaaS 開発に携わるエンジニアの話が聞いてみたい方 イベント内容の詳細は以下をご確認ください。 rakus.connpass.com 発表内容のご紹介 SRE課が開発中システムのCI/CDで取り組んでいるGitOpsの話 ラクスサービスを支えるAnsible活用のこれまでとこれから メール配信サービス「blastmail」のM&A後の軌跡 ~初めてのシステムに向き合う~ 終わりに 発表内容のご紹介 SRE課が開発中システムのCI/CDで取り組んでいるGitOpsの話 登壇:今本 光 [所属:SRE課] speakerdeck.com 発表内容 アプリ開発 において、面倒な作業はなるべく自動化して作業を楽にしたいと考える方も多いと思います。 今回は、新規システム構築というチャレンジしやすい環境の中で導入したGitOpsというCI/CDの手法についてご紹介。 GitOpsを導入すると、Git操作だけで自動デプロイが行われるので、デプロイの 工数 削減や安全性向上に繋がります。 主なテーマは以下になります。 CDツールArgoCDを使ってGitOpsに取り組んでいる話 GitOpsの実現方法 GitOpsによって得られるメリット ラク スサービスを支えるAnsible活用のこれまでとこれから 登壇:上畑 圭史 [所属:大阪インフラ開発課] speakerdeck.com 発表内容 2018年から開始したAnsibleによるサーバ構成管理。 導入から4年が経ち、それまでに整備したAnsibleを支える周辺環境の構成や取り組み内容をご紹介。 Ansible導入期 目的と背景 Ansible普及期の取り組み内容 今後の課題 まとめ メール配信サービス「blastmail」の M&A 後の軌跡 ~初めてのシステムに向き合う~ 登壇:柏木 達仁 [所属:東京インフラ開発2課/課長] speakerdeck.com 発表内容 M&A が流行っている昨今、 ラク スが M&A で取得したメール配信サービス「blastmail」について、 M&A 後にシステム側が何をしていったのかをトピックスでご紹介。 M&A に関わらず、「初めてのシステム」を担当する際の参考になればと思います。 M&A 直後 -システム移管~自社運用化- 改善その1 -監視の改善- 改善その2 -運用の自動化- 改善その3 -コスト削減- まとめ イベント当日はたくさんの方にご視聴、そしてコメントやご質問をいただきました。 お申し込み、ご参加いただいた皆さま本当にありがとうございました! 終わりに ラク スMeetupでは現場最前線のエンジニア/デザイナーから ラク スの SaaS 開発ならではの技術・運用ノウハウや、 新しい取り組みの成果や失敗談、プロダクト開発/運用で得た知見等の技術情報をお届けしております。 今後も定期的なイベントを計画しております、ぜひご参加ください。 最後までお読みいただきありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申し込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申し込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com