夏インターンでPHPStanのバグを直してコントリビュートした話

こんにちは。先日PIXIV SUMMER BOOT CAMP 2023にpixivウェブエンジニアリングコースで参加した、zer0-starです。

インターン期間中に、メンターのtadsanによりPHPStanのバグが発見され、僕がそれを直しました。

せっかくなので、直したバグについて話していこうと思います。

なお、今回出したPRは、以下になります。

https://github.com/phpstan/phpstan-src/pull/2591

バグの概要

僕が直したバグの概要ですが、端的に言うと「array_sum()に渡される配列の要素の型が定数型を含んでいたときなどに、推論される返り値がありえない型になる」というものです。

array_sum()とは、その名のとおり、引数として渡された配列の和を返す関数です。

www.php.net

定数型とは、1 2.3 'foo' のように、定数を表す型のことです。TypeScriptでお馴染みかもしれませんね。

例えば、"整数の配列"を表すarray<int>という型の値がarray_sum()に渡されたときは、返り値がintと推論されていました。これは正しい結果です。

しかし、例えば"要素が全て1であるような配列"を表すarray<1>という型の値が渡されたときには、期待される返り値の型は"0以上の整数"を表すint<0, max>か、少なくともそのスーパータイプであって欲しいでしょう。にもかかわらず、実際に推論された型は1となっていました。この結果は明らかに正しくありません。

他にも、例えば[1, 2, 3] が渡されたときには返り値が6と推論されて欲しいですが、実際には1 または2 または3 を表わす 1 | 2 | 3 と推論されてしまっていました。困ります!

では、なぜこのようなバグが発生してしまったのでしょうか。それは、array_sum() の型を推論するためのPHPStan拡張の実装に問題があったからです。

元の実装では、要素の型をほとんどそのまま返すようになっていました。これは、定数型が渡されなければ、問題なく動作する実装でした。つまり array_sum() の入力が array<int> なら intarray<float> なら floatarray<int|float> ならば int|float を返すということです。

これは一見よさそうですが、array<1|2>array_sum() に入力した結果は 1|2 ではありません。要素が1と2しかなかったとしても、空のリストならば結果は 0 、長さが2以上ならば結果はそれに応じて増加していきます。

この拡張の実装時には int[]non-empty-array<float> のような型のテストしかなく [1, 2, 3] のような定数を直接渡していなかったため、この問題はこれまで見過ごされていたようです。

PHPStan拡張とは

ここで登場したPHPStan拡張というものについて説明します。(ここでは、型推論に絞って話します)

実は、PHPStanは素の状態では型の表現力が(TypeScript等と比べて)あまり高くありません。

しかし、それを補う仕組みとして、ユーザーやライブラリの作成者が、PHPStanの静的解析をアドホックに拡張する拡張機能を作成することができます。拡張機能では、例えば関数やメソッド単位で型推論をオーバーライドして、通常では不可能な精密な型付けをすることができます。

型の表現力を強くする代わりに拡張機能という形式をとっていることのメリットとして、型付けのルールをPHPのコードとして書くことができるので、複雑な型付けも読み書きしやすく、型を操作するための専用の構文などに習熟する必要がないというものが挙げられます。

array_sum() の返り値の型推論では、PHPStan本体に同梱されているarray_sum() 専用の拡張機能が使用されています。先程のバグは、この拡張機能が原因で発生していたのでした。

PHPStan拡張ってどうやって実装するの

関数の型推論をする拡張機能は、具体的には関数の定義、関数適用の構文木、そして型環境をとって型を返すようなメソッドとして実装できます。例えば、「ある関数について返り値の型を引数全ての型のunionに推論する」ような拡張機能を作りたいなら、次のようなメソッドを書くことになるでしょう。

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
    $resultTypes = [];

    foreach ($functionCall->getArgs() as $arg) {
        $resultTypes[] = $scope->getType($arg->value);
    }

    return TypeCombinator::union(...$resultTypes);
}

コードの詳細については説明しません。型をデータとして操作していることが分かればOKです。

Scope::getType() を使うことで、自分の関心がない箇所についてはPHPStanの側に型推論を任せ、拡張側では型を組み合わせて最終的な型を作ることに専念できます。

実際にバグを直してみる

ここまで分かれば、あとは拡張を正しい動作をするように書き換えるだけです。

APIリファレンスとにらめっこして……よし、書けた!大量に追加したテストケースも通ったし、PRを出すぞ!

そうしてできたのが、こちらのコードです。

https://github.com/phpstan/phpstan-src/pull/2591/commits/2bef891930282fa1bb347590f9ab2a0404fb1cd6

配列の形やunionを全て場合分けで対応しているので、複雑なコードになってしまいました……泣

無限の場合分けと無限のネストを兼ね備えたコード

これに対してPHPStanの作者のOndřej Mirtes氏からのフィードバックは、以下の通りでした。

Hi, I don't really love that this PR is working with specific scalar values and calls array_sum internally. It misses out on various properties of the type system, like working with IntegerRangeType.

I'd much rather be if for ConstantArrayType objects, like [$one, $two, $three], it tried to do $scope->getType(new Plus($one, new Plus($two, $three))). Because Plus expr handling in MutatingScope already knows how to work with al possible Types, we can just call it here like that.

Because ConstantArrayType contains Type and not Expr, you can take advantage of TypeExpr virtual node to build an Expr out of Type instances.

つまり、要素の型がunionか定数かintかfloatかといった情報をいちいち手動で場合分けするのではなく、PHPStanの型推論に任せようということです。

そのために使える機能として、TypeExprというクラスを使うといいというアドバイスをもらいました。

ReturnType拡張を作成する際に頻出するPHPStan\Analyser\Scope::getType()というメソッドは、ソースコードをパースしたPHP Parserの構文木のノードを受け取って型を解決できます。1 + 1と書かれた式のノードを渡すと、定数型のint<2>rand() + rand() なら int が得られるということです。ここまでは普通の使い方なのですが、PHPStanはVirtualNodeというPHP Parserを拡張した独自のノードを提供しています。TypeExprはVirtualNodeのひとつで、PHPStanのTypeオブジェクトをPHP Parserのノードとして扱えるようにするためのアダプターです。

つまり、array_sum([1, 2]) というコードの結果は $scope->getType(new Add(new LNumber(1), new LNumber(2))) のようなノードを組み立てることで得られますが、new LNumber(1)のような部分を別の部分から $scope->getType() で得られたTypeオブジェクトをTypeExprでラップしたものに置き換えても型を解決できるということです。これを活用することで、要素が変数か定数かといった細かな場合分けを完全に廃することができました。このTypeExpr については、PHPStanを"完全に理解した"というメンターのtadsanも初めて聞いた機能だったとのことです。

その後も修正とフィードバックを繰り返し、コードの質だけでなく型推論の性能も高くすることができました。

そして、インターン最終日の最終課題発表をしている最中に、PRがマージされました!ありがとう……

おわりに

PHPStanでは容易に拡張機能を書いて静的解析の効果を上げることができます。みんなも、お気に入りの拡張を育てて自分だけのPHPStanを作りあげよう!

2段階認証のバックアップコードの仕様をどのように決めたか紹介します

こんにちは、プラットフォーム開発部で認証認可基盤の開発を担当しているabcangです。

先日pixivのログイン画面で2段階認証が利用できるようになりました。現時点ではTOTP(Time-based One-Time Password)認証アプリとバックアップコードの2種類が2要素目の認証手段として利用できます。バックアップコードは、メインの2要素目の認証手段が使えなくなった際に代わりの認証手段として使用できるコードで、2段階認証を有効化したときに生成しています。

バックアップコードは長ければ長いほど安全ですが、長いといざ使うときに入力が大変ですし入力ミスもしやすくなります。しかし、逆にバックアップコードを短くしすぎるとセキュリティ的に問題になるため、利便性とセキュリティのバランスを取る必要があります。

今回はこのバックアップコードの仕様をどのように決めたかをご紹介します。

NIST SP 800-63Bを確認する

NIST SP 800-63は、アメリカ国立標準技術研究所(NIST)が発行している電子認証のガイドラインです。これに含まれるNIST SP 800-63Bには認証プロセスに関する技術要件や推奨事項が記載されており、認証機能を実装する際に非常に役に立ちます。また、OpenID Foundation Japanによって日本語訳版も公開されています。

バックアップコードはこのガイドラインのルックアップシークレットに該当します。認証には知識要素(Something you know)、所有要素(Something you have)、生体要素(Something you are)の3種類ありますが、ルックアップシークレットに該当するバックアップコードは所有要素に分類され、知識要素であるパスワードとは要素が異なっています。そのため、パスワード+バックアップコードを使う場合でも2要素を使用した認証ということになります。

pixivを含む多くのサービスではバックアップコードは1回しか使えないようになっていますが、このガイドラインにはSHALL(必須要件)として記載されています。そして、ルックアップシークレットのエントロピーに応じた保存方法やレート制限などのセキュリティ要件についても細かく記載されています。

エントロピーに応じたセキュリティ要件

NIST SP 800-63Bからルックアップシークレットのエントロピーに関する記載を抜粋したものが以下になります。

  • ルックアップシークレットは最低20ビットのエントロピーを持つものとする(SHALL)
  • 112ビット以上のエントロピーを持ったルックアップシークレットは,Section 5.1.1.2に記載されているようにApprove済み一方向関数でハッシュ化されるものとする(SHALL)
  • 112ビット未満のエントロピーを持ったルックアップシークレットはSection 5.1.1.2に記載されているように,ソルトを追加したうえで適切な一方向の鍵導出関数を用いてハッシュされるものとする(SHALL)
  • 64ビット未満のエントロピーを持つルックアップシークレットに対して,Verifierは,Section 5.2.2に記載されているように,SubscriberのアカウントにおけるAuthentication失敗回数を効果的に制限するレート制限の仕組みを実装するものとする(SHALL)

64ビットを境にレート制限が必須かどうかが、112ビットを境にハッシュ化するときにソルトが必須かどうかが変わってきます。

ビット数だと分かりづらいので文字数で換算してみます。1文字あたりのエントロピーのビット数は log_2(文字の種類数) で算出でき、1文字あたりのビット数で割れば必要な文字数がわかります。他社事例で多かった「数字のみ10種類」「小文字英数字36種類」「小文字英数字32種類(小文字英数字36種類から紛らわしい4文字を除外)」の3パターンで換算したものが以下になります。

数字のみ10種類 小文字英数字32種類 小文字英数字36種類
1文字あたりのビット数 3.3ビット 5ビット 5.1ビット
20ビットに必要な文字数 6.02文字(7文字) 4文字 3.8文字(4文字)
64ビットに必要な文字数 19.2文字(20文字) 12.8文字(13文字) 12.3文字(13文字)
112ビットに必要な文字数 33.7文字(34文字) 22.4文字(23文字) 21.6文字(22文字)

括弧書きした文字数は小数点以下を切り上げた場合の数字です。基準のビット数を下回らないようにするには、小数点以下を切り上げて考える必要があります。

数字のみだと種類数が少なく、エントロピーを高くするには多くの文字数が必要になります。そのため、文字数を少なくしたい場合には数字のみにするのは向いていないです。

小文字英数字については、112ビット以外は32種類と36種類の文字数の差はなさそうです。そして、レート制限必須化のラインである64ビットのエントロピーには、小文字英数字だと13文字以上が必要なことがわかります。バックアップコードは何文字かごとにハイフンやスペースで区切って見やすくすることが多く、それも考慮する場合は適度な数で割り切れる15文字や16文字にすることになりそうです。

大文字の英字も含めて文字種を増やせばバックアップコードの文字数を減らすことができそうですが、大文字を入力するにはShiftキーも押す必要があり、大文字と小文字が混在しているとなおさら入力しにくくなるというデメリットがあります。

検討した結果、以下のような理由から小文字英数字を12文字の長さで使用することに決めました。

  • レート制限に関係なくエントロピーはなるべく高くしたい
  • 小文字英数字で15文字や16文字の長さになると入力ミスをしやすくなりそうなので避けたい

小文字英数字の12文字だと64ビットを少し下回ってしまうためレート制限の実装が必須になります。よほどエントロピーが高くない限りはレート制限もしておくほうがより安全だろうと考えていたため、レート制限を実装することに抵抗感はありませんでした。また、112ビットに達していないためバックアップコードはソルト付きでハッシュ化して保存することになります。

紛らわしい文字を除外

数字と小文字英字の中には 1 (数字のいち)と l (小文字のエル)のように紛らわしい文字の組み合わせがあります。バックアップコードの入力ミスを防ぐため、こういった文字を除外することにしました。

先程も登場しましたが、小文字英数字から4文字を除外した「小文字英数字32種類」を採用している他社事例がありました。英数字32文字と言うとBase32(RFC 4648)が一番有名だと思いますが、これは大文字を前提として数字の0、1、8、9を除外しています。また、英語版WikipediaにはBase32の亜種がいくつか載っており、手書きすると紛らわしくなる文字( v u r 2 z )も考慮しているz-base-32や紛らわしい文字を同一の文字として扱うCrockford's Base32など様々なバリエーションがあることがわかりました。バックアップコードを厳密なBase32として扱いたいわけではなく、紛らわしい文字の扱いの参考にするために調べていました。

Base32やBase32の亜種では紛らわしい文字の組み合わせの片方だけを除外するケースが多く見られますが、ユーザーはどの文字が除外されているか知らないため、紛らわしい文字が含まれていた場合は迷いながら入力することになってしまいます。紛らわしい文字を同じ文字として扱えば入力に失敗することはありませんが、入力するときに迷ってしまうこと自体は解決できず、UXとしてはいまいちな気がしました。そう考えて、より見た目が似ている以下の4文字を除外することにしました。

  • 0 (数字のれい)
  • 1 (数字のいち)
  • o (小文字のオー)
  • l (小文字のエル)

バックアップコードはダウンロードやスクリーンショットで保存してもらうことを想定していて、手書きで書き写すケースは稀だと考えたため、手書きのときに紛らわしくなりやすい他の文字はそのまま採用しています。

最終的な仕様

セキュリティ的な要件と利便性を考えて、pixivでは2段階認証のバックアップコードを「 0 1 o l )を除いた小文字英数字32種類」の12文字で構成することに決めました。32種類の文字を12文字の長さにする場合は log_2(32) * 12 で60ビットになるため、ソルト付きでハッシュ化して保存し、レート制限の仕組みを実装しています。

また、文字列が連続していると見にくいため、バックアップコードを表示する画面では4文字ごとにスペースで区切った xxxx xxxx xxxx のようなフォーマットで表示しています。実際にバックアップコードを検証するときにはスペースを無視して処理しているため、スペースはエントロピーには影響を与えていません。

おわりに

セキュリティと利便性の両方を考慮してバックアップコードの仕様を決めることができました。今後もセキュリティを維持しながら利便性を向上できるように認証認可基盤の改善に取り組んで行きたいです。

ピクシブでは認証認可に興味のあるエンジニアを募集しています。

hrmos.co

レポートpixiv MANGA Night 2

こんにちは!コミック事業部エンジニアのKNRです。

先日、「インフラコストを根本から削減する」株式会社DELTA様との共同でイベントを実施したため、その模様をレポートします!

pixiv MANGA Nightとは?

ピクシブの4つのマンガに関係するプロダクト(「pixiv」、「pixivコミック」、「pixivコミックインディーズ」、「Palcy」の裏側について、ユーザーやエンジニアの皆様に知っていただけるよう開催しているイベントです!前回は第1回として4つのプロダクトから4人のエンジニアが登壇し、それぞれのプロダクトの裏側について発表させていただきました。前回の模様に関してはこちらを御覧ください。 inside.pixiv.blog

今回は第2回として「Palcy」のAWSについてピックアップし発表させていただきました。「Palcy」は講談社様とともに提供しております、女性・少女マンガアプリでiOS/Android両スマホプラットフォームで配信しています。バックエンドはRuby on RailsとAWSを利用しており、ピクシブのサービスの中では珍しくオンプレ環境ではなく完全にクラウドの環境で構築されています。

www.youtube.com

第一部 Palcyのコスト削減プロジェクトについて

第一部では今回のメインとなるPalcyのコスト削減について株式会社DELTAの馬場様とともに発表を行いました。株式会社DELTA様では「CTO Booster」という「サーバー代削減のための調査・検証・作業をまるっと代行」するサービスを提供しており、弊社は増大するインフラ費用の削減を「CTO Booster」で実施しておりました。このセクションでは、そこで行った3つの施策について発表がありました。

ECSへの施策

1つめの内容はECSへの施策でした。PalcyではコンテナのマネジメントサービスAmazon ECSをAmazon Fargateを用いてピーク時・ピークアウト時の調整を実施しています。このECSにおいて確保するリソースが過大な部分があって遊んでいるリソースが存在している状態でした。そこでこのリソースを適切なサイズに変更することで、40%のコスト削減を実現しました。

この施策の提案はかなりはじめの方に行われたのですが、私は「いきなりヘビーな話を…!」と感じたことを覚えています。とはいえPalcyのデプロイフローにおいてはG/Bデプロイを採用しているのでECS自体が全く動かなくなることや、失敗した場合でも切り戻しは可能であったこと、また開発環境で十分に検証できたことで特にダウンタイムなくリリースができること、また現行のアプリケーションへの影響がないことを確認できたため、本施策を本番リリースしました。本件に関して馬場様とKNRの間で綿密に連絡を取り合いながら施策を進めました。

Log出力への施策

2つめの内容はCloudWatch Log出力への施策でした。具体的にはヘルスチェックAPIの起動・終了時に出力されるログが大量に出力されている状態であったものの、アクセス状況やレスポンス時間はCloudWatchのメトリクスですでに監視できる状態にありさらに、ログ自体は直接見ているわけではありませんでした。なので、この無駄になってしまっているログ出力を抑えてコストカットを行う施策を実施しました。

実装時にこのログに対する意識があまりなかったので、無駄になっているということが提案いただくまで気づかずなるほど…!となりました。基本的に出力されたログは開発時には1つ1つみていますが、安定運用に入った段階で例外などの文字列をトリガーとして自動的にSlackに発報する仕組みをとっているため完全に誰も見返すことのない無駄なログになっていました。

データ通信量への施策

最後の内容は、データ通信量への施策でした。Palcyはスマートフォンアプリであるため、モバイルアプリとWebアプリケーションの通信は予め定義した内容のJSONをWebAPIでモバイルに渡します。ただこのJSONファイルの内容がかなり大きく、最大で1MBを超える場合もありAWSの通信料もユーザーさんの”ギガ”も使ってしまう状況にありました。しかし、汎用的なモデルを使いまわしていたこともあり、APIから返された内容がすべてモバイル上で使われているわけではなかったためモデルの最適化を行いレスポンスの内容を60%程度削減しました。

この施策はPalcyチームで元々計画をしていたものでしたが、日々の開発や突発的に発生するバグなどの対応に追われチームとして足並みを揃えてこの対応を行うことが難しいなかでDELTA様に対応していただきました。対応に当たってiOS、Android、Webの3つを対応していただけたことは大きかったと思います。また同時にiOS/Android間でのデザインのズレにも気が付き同時に対応していただきました。

プロジェクトを通して

プロジェクト全体を通して費用削減額として約$5,000/月の大きな成果を上げていただきました。これは予想していたよりも大きな金額だったと思います。

KNRからは下記2点セキュリティ観点上の理由から難しかったこと

  • 2社間でのGitを使ったコード共有が難しく、この点の解決に時間がかかってしまったこと
  • 必要十分なIAM権限の付与のため、何度かやりとりを往復しなければ行けなかったこと

と、良かった下記2点

  • 「CTO Booster」は料金の削減額に応じて支払額が決めるため、弊社としてのメリットが大きかったこと
  • 仕様を伝える上で過大に資料を作って渡す必用がなく負荷が少なかったこと

をプロジェクト全体を通して感じたこととしてお伝えしました。

Palcyでは、まだまだアーキテクチャ等で改善をしていきたいと考えている事があり、例えば

  • RDSからDynamoを活用する形にできないか
  • 様々な画像や作品を取り扱うことをメインとしているため転送量が大きくなりがちなため、これらの転送量をどう減らすべきか
  • サーバレスアーキテクチャをもっと導入できないか

など将来のお話をさせていただき、このセクションを〆とさせていただきました。

第二部 pixivの開発者たちが今取り組んでいる挑戦について

第二部では「pixivコミック」、「pixivコミックインディーズ」、「Palcy」を束ねるコミック事業部のマネージャーであるtenと「pixiv」マンガのマネージャーであるuchienより各プロダクトで今挑戦していること、また開発している技術の話をしました。

コミック事業部

tenからは、多数の出版社と提携した商業連載プラットフォーム「pixivコミック」、pixiv投稿作品を編集者に見てもらえるマッチングサービス「pixivコミックインディーズ」の紹介を行いました。ピクシブが展開するマンガサービスでは、クリエイターの皆様の活躍の場をつなげるために複数のサービスを展開しています。そのうち「pixivコミック」、「Palcy」は商業作品としての発表の場として、「pixivコミックインディーズ」は商業へとつなげる場としてサービスを展開しております。

「Palcy」を含めたプロダクトではエンジニアを募集しており、「pixivコミック」ではフロントエンドをReact/Next.js、バックエンドをRuby on Rails、「pixivコミックインディーズ」ではフロントエンドをReact/Next.js/vite、バックエンドをPHPのエンジニアをそれぞれ募集しております。

pixivマンガチーム

uchienからは、pixivマンガチームにおいてUIの開発のお話や、創作コミュニティ盛り上げのために行った「学生マンガデイ」(https://www.pixiv.net/special/gakuseimanga/)、「サークルスペースメーカー」(https://www.pixiv.net/special/circlespacemaker/)など関連したイベントやプロダクトの紹介を行いました。

マンガチームではフロントエンドはVue/React/Next.js、バックエンドはPHPを使っており、両エンジニアを募集しております。

第三部 まとめ、pixivの今後の展望について

最後に本日のまとめとして、第一部・第二部のふり返りを行い、最後にピクシブ全体でエンジニア、特にAWSやクラウドエンジニアをどう捉えているかのお話をさせていただきました。 pixivのイメージとして古くはベニヤ板サーバーの時代などオンプレのイメージが強いかと思います。しかし「Palcy」を始めとしてクラウドを活用を進めております。例えばこちらの「GitLab GCPに 移行した(前編)」(https://inside.pixiv.blog/2022/11/29/110000)など、クラウドエンジニアの活躍の場は増えております。

ぜひ我こそは!という方は下記コーポレートサイトより応募をしていただければと思います。

https://www.pixiv.co.jp/

Next.jsのプレビュー環境をプルリクから自動で構築する

こんにちは、アルバイトのciffeliaです。

pixivは、2022年にNext.jsによるフロントエンドのリプレイスを開始しました。2023年現在では、10人以上の開発者がNext.jsによるフロントエンド開発に参加しています。

この記事では、Next.jsプロジェクトにおけるプルリクエスト*1の動作確認を効率化する、PRプレビュー環境自動構築の仕組みについてご紹介します。

何をしたのか

プルリクエストを作成すると、対象ブランチの動作確認をするためのURLが自動で発行される仕組みを作りました。 赤枠で囲まれたボタンをクリックすると、対象PRのプレビュー環境にアクセスするためのCookieがブラウザに記憶されます。この状態で動作確認したいページを開くことで、PRにおける変更を実際に検証することができます。

背景

pixivは2022年にNext.jsの導入を開始しました。2023年7月現在では約50のページ*2がNext.jsで実装されています。

inside.pixiv.blog

フロントエンドの開発においては、レビュアーやデザイナーがブラウザ上で動作確認を行う場面が多々あります。これまでNext.jsプロジェクトにおける開発では、次の手順でレビューを行っていました。

  1. 開発者:PRを作成
  2. 開発者:共用開発サーバーにSSHでログインし、適当なポートでNext.jsを起動
  3. 開発者:PRの説明欄にポート番号を記入
  4. レビュアー:コードをレビュー
  5. (必要に応じて)レビュアー・デザイナー等:ポート番号を指定して開発サーバーにアクセスし、動作を確認

この中で開発者は共用開発サーバーでNext.jsを起動する必要がありますが、この手順は単に手間がかかるだけでなく、次のような問題点がありました。

  • 開発サーバーの再起動によりNext.jsが停止してしまう。
  • Next.jsを起動していることを忘れてしまい、ゾンビプロセスになって開発サーバーのリソースを消費する。
  • 複数のレビュー依頼を並行して行う際には、開発サーバー上でリポジトリのクローンを複数用意する必要がある。

この問題を解決する策として、VercelやNetlifyのようにプルリクエストからプレビュー環境を自動的に起動する仕組みを開発することにしました*3

Next.jsを自動で起動する仕組み

次のように、複数の段階に分けて検証環境が構築・破棄される仕組みになっています。

  1. プルリクエストが作成されると、CIでコンテナイメージがビルドされる。
  2. 検証環境にアクセスがあると、イメージからコンテナが作成される。
  3. 5分程度アクセスがないコンテナは自動で停止される。
  4. 古いコンテナイメージは一定期間後に削除される。

検証環境サーバーのリソースを節約するため、コンテナはアクセスを受けたタイミングで起動する仕組みになっています。このアイデアはサーバーレスアーキテクチャから着想を得ています。初回アクセス時のコンテナの起動に要する時間が気がかりでしたが、同時に進めていたイメージサイズ削減の効果もあり、10秒程度で起動するようにできました。

また、コンテナの起動と停止やリクエストのルーティングは既存のソフトウェアの組み合わせで実現するのが困難だったため、専用のアプリケーションを開発して実現しました。TypeScript + Node.jsで実装しています。

PR作成からリンク表示までの流れ

検証環境へのアクセスの流れ

おわりに

プレビュー環境の自動構築により、多くの開発者のコードレビュー依頼における負担を軽減することができました。フロントエンド開発における開発者体験向上の事例として参考になれば幸いです!

*1:GitLabではMerge Requestと呼びますが、この記事ではプルリクエストまたはPRと書くことにします。

*2:Next.jsのrouteの数(ページコンポーネントの数)です。

*3:バックエンドとの接続などの問題があり、Vercelを導入することは困難でした。

【2023.4.20】モバイルアプリのウラ側を公開!pixiv App Nightを開催しました

ピクシブ株式会社は、モバイルアプリエンジニアが一堂に会して、 モバイルアプリ開発に関する知見を共有するイベント「pixiv App Night」を定期的に開催していく予定です。

イベントではiOS / Androidのエンジニアが、クリエイターの創作活動を支えるアプリのウラ側を質疑応答も交えながら、ざっくばらんにお話しさせていただきます。

本日は4月6日に開催したイベントの登壇内容を皆様にご紹介したいと思います。

イベントログ

pixiv App Nightは隔月のペースでアプリエンジニアが一人当たり10〜20分程度で知見を発表する、オンラインイベントです。

過去開催したイベントについて記事化しておりますので、ご興味あれば併せてご覧ください。

登壇内容について

先月4月6日はAndroidエンジニア4名が、普段の開発知見に関する発表を行いました。登壇資料とイベント時の録画映像はconnpass上に掲載しておりますので、下記リンクよりご覧ください。

イベントに興味をお持ちの方は

次回の開催は6月を予定しています。募集はconnpassを経由して行いますので、宜しければpixivのconnpassページをフォローいただき、案内をお待ちいただけますと幸いです。

また今後もブログだけでなく、YouTubeにて登壇内容の発信を行いますので、宜しければ併せてチャンネル登録もお待ちしております。

https://www.youtube.com/channel/UCoeizGz7hQa2MnppFPSDSWA

子どもに贈りたい、ブックサンタで寄付した本

NPO法人チャリティーサンタが主催し、「厳しい環境にいる全国の子どもたちに本を届けること」を目的に活動している「ブックサンタ2022〜書店で誰でもサンタクロースに〜 | NPO法人チャリティーサンタ

パートナー書店で好きな本、またはブックサンタのオンライン書店に掲載されている本を購入すると、全国の子どもたちにサンタクロースが本を届けてくれるというチャリティープログラムです。

pixivではこちらの「ブックサンタ」と協力して「pixiv小説子どもチャリティー企画 ブックサンタ」というコンテストを開催中。 オリジナル、ファンフィクション問わず、クリスマスをテーマにした小説やエッセイに「ブックサンタ2022」のタグをつけて投稿すれば、ピクシブ株式会社がNPO法人チャリティーサンタへ1作品ごとに500円寄付します。

12月20日現在、450名を超える方に参加いただいています。手前味噌ですが、このコンテストのいいところは、オリジナル、ファンフィクションを問わないのでいつもの投稿にタグをつけるだけという手軽さ、何より自分に寄付するだけの金銭の余裕がなくても投稿すれば子どもたちを支援できるというところ。 12月25日までの開催となっているので、一から執筆するのもよし(文字数制限はありません)、ちょうど投稿するクリスマスをテーマにした作品があるのなら「ブックサンタ2022」のタグをつけて投稿してみてください。

https://www.pixiv.net/novel/contest/booksanta2022

さて、宣伝はそれくらいで、今回はこの「ブックサンタ」の取り組みに賛同した社員たちが、パートナー書店で選び寄付した本の一部をご紹介をしていきます。 私は毎年ブックサンタに参加していますが、これまではブックサンタのオンライン書店で運営の方々が選んだ本の中から寄付していました。自分で一から選ぶのは初めてです。自分の子ども時代を思い出してどんな本が好きだったかとか、これなら喜んでくれるんじゃないだろうかとか、「誰かのために本を選ぶ」という体験は楽しかったです。それに他の社員と行ったので「それ選ぶんだ」「子どものときそういう本好きだったんだ」と、選びながら盛り上がりました。

寄付は12月24日まで受け付けているそうです。この記事でブックサンタの取り組みを知って興味がわいたなら、パートナー書店もしくはブックサンタのオンライン書店からの寄付、pixivへの投稿をお願いします。

ピクシブ社員が寄付した本

www.amazon.co.jp 恵泉女学園を創設した河井道と、元教え子の一色ゆりとのシスターフッドの物語です。ふたりは生涯にわたって支え合い、日本の女子教育のため尽力しました。これからたくさんの人に出会うであろう若い子が、将来ふたりのような関係を誰かと築けるかもと想像してワクワクするんじゃないかと選びました。 また、河合道の生涯を知って、教育が彼女の世界をとんでもなく広げてくれたのだなと感じました。いま勉学に励んでいる子たちの支えにもなる本だと思います。(hotep)

www.amazon.co.jp 幼少期はインターネットがなかったので、暇つぶしといえば読書でした。なかでも図鑑は写真や絵が多く、眺めているだけでも楽しくて大好きでした。「深海生物」を選んだのはロマンがあるからです。深海は現代でもわからないことがたくさん! この図鑑をきっかけに、将来深海の謎を解き明かしてくれたらうれしいです。 私は10年近く前にNHKスペシャルで見た、深海生物ダイオウイカの目が忘れられません。知性を感じる、世界について何もかも知っているような目をしていました。ダイオウイカは何を知っているのか、その答えを待っています。(hotep)

www.amazon.co.jp 「小学館の図鑑NEO 宇宙」を寄付させてもらいました。自分の大好きな作詞家・作曲家さんが宇宙をモチーフにした最高にクールな曲を作られるんです。この本を手に取ってくれた方が宇宙の魅力に気が付いてくれて、何らかの形で将来のアウトプットに繋がればいいなと思い選びました。 ビジュアル豊富で美しい恒星や銀河の姿が伝わる一冊だと思うので、じっくり読んでもらえたらうれしいです。(alitaso)

www.amazon.co.jp クリスマスの大仕事を終えたサンタさんのバカンスを描いたマンガ作品です。 可愛らしいサンタのビジュアルと色鮮やかなアナログ表現が可愛らしく、ワクワクするのと同時に心が温かくなります。 色、絵柄、描かれたもの、なにか心に残る表現がこの本を手にとった方にお気に入りとして残ってもらえると嬉しいです。 何かを見たとき、体験したときに心に残るものはきっと創作や自己表現の種になるはずです。この本も是非楽しんで読んでいただければと思います。(chaka)

www.amazon.co.jp 先月までpixivで開催していた「エモい古語辞典小説コンテスト」のお題となっていた古語辞典になります。「好きなキャラをエモく表現するために感受性を爆上げしたい」という中学生の要望から生まれた本書は、「海月の骨」「可惜夜」「金襴の契り」など想像力が刺激される「エモい古語」が多数掲載されています。

きらきら輝くような古語をたくさん吸収して好きな小説や漫画の世界をより楽しめるようになったり、自分の気持ちや考えを”エモく”伝えられるようになる一助となれば、と選書しました。何より眺めているだけで楽しく時間が溶けてしまう読み応えのある辞書となっています。ことばの魅力をたっぷり堪能していただけたら嬉しいです。(koda)

www.amazon.co.jp バレエ教室に通い始めて5年が経っても、踊りが上手くならない女の子。ある朝、彼女に小包が届き……。 大人になるにつれ心に残ってる絵本というのも大分少なくなってきましたが、この作品だけは、美しい挿絵やハッとするお話、ページをめくる時のわくわくした気持ちを覚えています。 この絵本を読んでバレエに興味を持った……ということもなく、踊りとは無縁の育ち方をしましたが、挫折をした時や頑なな時、あの幻想的な世界のことを心に思い浮かべて乗り越えていた日々は今でも大切な思い出です。 ふわりとしていても芯のあるこの世界が、子どもの柔らかな心に寄り添ってくれると良いなと思って贈りました。(risari)

pixivをNext.jsでリプレイスする取り組みをご紹介します。

pixivではNext.jsを用いたフロントエンドのリプレイスプロジェクトを2022年3月末より行っており、現時点(2022年8月)でリクエスト機能をNext.jsにてリプレイスしました。

今回のpixiv insideではピクシブ株式会社で働くエンジニアの取り組みとして、pixivのフロントエンドをNext.jsでリプレイスする取り組みについて実際に取り組んだメンバーからご紹介します。

まずは皆さんの自己紹介をお願いします

namazu:
pixivのウェブ領域に関するテックリードを担当しているnamazuです。今回のNext.js化プロジェクトではPjMやNext.jsのホスティング回りの実装を担当しています。
shu:
2022年3月に入社したshuです。Next.js化ではフロントエンドの設計、実装を担当しています。
mog:
エンジニアとしてアルバイトをしているmogです。Next.js化ではフロントエンド実装の補助をしています。

Next.js化に取り組むに至った経緯を教えて下さい

namazu:
プロジェクトの経緯についてはnamazuの方からご説明します。

pixivは2018年頃から既存のPHPテンプレートエンジンで描画しjQueryなどを用いて動的な要素を実現している状態から、Reactなどを用いてSPA化を行ってきました。この際はNext.jsなどのフレームワークには乗らず、webpackなどの設定は基本的に1から、描画方法はPHPで描画したページに対してスクリプトを実行するいわゆるCSRの形で進みました。
このSPA化により、フロントエンド領域の技術を活かすことが可能になり、高品質な体験や高速なアップデートの実現、また開発規模の拡大が容易になりました。
しかし現在では2018年からすでに4年立ち、サービス・開発規模のさらなる拡大に伴い新たな問題も出てきました。
フロントエンドについては大きく2つあげられます。

1つはpixivがレスポンシブではないことです

昔の話ですが、pixivは2007年にPC向けにサービスインし、その数年後にスマートフォン向けにサービスを開始しました。このときPC向けとスマートフォン向けのpixivはホストしているドメインが異なり、バックエンドの実装は一部を共有するものの、フロントエンドに関してはソースコードリポジトリは同じでしたが実体としては別のアプリケーションとして実装されました。
その後2018年始にpixivのデスクトップブラウザ向け実装とスマートフォン・モバイルブラウザ向け実装をホストするドメインを現在の www.pixiv.net に統合しました。しかしアプリケーションをレスポンシブ対応し実装を統合することは様々な事情から難しく、その流れを引いて現状でもpixivの主要機能はデスクトップ版とモバイル版に別れて実装されています。
機能追加や変更にあたって双方の実装に対して変更を行う必要があり、工数が増加・また双方の機能差異なども存在しサービスが複雑化しています。
長期的に見て、この状態は好ましくないため、pixivのウェブ実装を統一し複雑さを下げていく動きが必要です。その過程でNext.jsによってフロントエンドをリプレイスしつつレスポンシブ化を進めていくことが現実的な解となりました。

pixivの実装の歴史 / 2022-02 PIXIV TECH FES Keynoteより

2つめはpixivのフロントエンドコードのリポジトリ規模が大きすぎることです

pixivのバックエンドは主にPHPで作られています。ピクシブ株式会社ではPHPで実装されているアプリケーションをコード資産の共有などを理由に基本的にpixiv.gitと呼ばれる1つのgitリポジトリで管理しています。pixiv.gitに含まれる代表的なサービスはpixivを始めとしてpixivFANBOXやpixivision, pixivアカウントサービスなどが存在します。

pixiv.gitとその中に存在する言語 / 2022-07

pixiv.gitのフロントエンドはPHPのテンプレートエンジンでページを描画し、必要ならjQuery、その後ReactなどでSPA化という流れをたどってきたので、当時都合がよかったpixiv.git内にフロントエンドコードが同居する形を取っており、pixiv.git全体でyarnを用いたモノリポジトリ構成になっています。それ故にpixivにおけるフロントエンドの大きな構成変更、ライブラリのバージョンアップ、また日々のデプロイなどはpixiv.gitに存在するサービスに互いに多少なりとも影響を及ぼすため変更にあたってはサービス間でのコミュニケーションが必要になります。

故にフロントエンドに関しては開発規模の増加に伴って

  • ライブラリのバージョンアップや大きな改善を全プロダクトで横断して進行することが難しい
  • メンバーがフロントエンドの改善にオーナーシップを持ち取り組めるようになるまで時間を要する
  • デプロイ頻度が上げられない

などの問題が起きてくるようになりました。

フロントエンドについてはサービス開発組織単位でコードリポジトリを分割し、各サービス開発チームがオーナーシップを持ってフロントエンドの改善に取り組んでいけるように、規模を小さくする必要があると考えました。
Next.jsへのリプレイスでは、将来的にバックエンドをAPIとして利用し、フロントエンドについてはリポジトリやホスティングをpixiv.gitから独立させることが可能なため、モチベーションとなりました。

これら2つの課題から、実際にNext.js化を進めていくことになりました。

実際にどのようにNext.js化をすすめたのですか?

shu:
実際の取組みにあたっては、実際にフロントエンド実装に取り組んだ私とmogの方からご説明します。

shu:
pixivは機能が多くコードも大規模なので、全てを一度に移植することは現実的ではありませんでした。またpixivのReactで書かれたコードも長く使用されており、”所謂”技術的な負債も存在します。そのため、コードベースを改善しながら小さくNext.jsへの移行を行うことにしました。

pixivはレスポンシブ対応していないという課題をあげましたが、実際は新しめのリクエスト機能などのページはレスポンシブ実装になっています。レスポンシブ対応の実装を入れつつ移植を進めるのはコストがかかるため。まずは既にレスポンシブ実装があるリクエスト関連のページから移植を行いました。

最初のリリースにあたっては、動作確認を行った上で、一部のページでABテストという形で一部ユーザに対してリリースしました。様子を見て問題がなければ、ABテストに当選するユーザの割合を上げていき、新しいページでまた低い割合のABテストをする。ということを繰り返しました。
また、PV数やユーザアクション数、パフォーマンスに大きな変動がないか確認をしながらリリースを行いました。

現在ではリクエスト機能関連ページはNext.jsになっています。

Next.jsになったリクエスト管理ページ

pixivをNext.js化する上で工夫した点やハマった点はありますか?

mog:
pixivではもともとCSRでRendertronを用いてDynamicRenderingを行っていたのですが、Next.js化する上でCWVやSEOを考慮してSSRを行うことにしました。元々CSR前提で書かれていたReactのコードでしたので、Node.jsでは使えないブラウザAPIなどを都度SSRに合わせて対応する必要がありました。

SSRについては、特に初回ランディング時に与えるユーザー体験への影響も考えられました。悪い影響の例としては、SSRとLocalStorageを利用したダークテーマ切り替えの組み合わせを起因とした顕著なフラッシュなどが問題になりました。これらの課題の対策には、ピクシブで開発しているOSSのデザインシステム実装「charcoal」の開発チームと協力し取り組みました。

inside.pixiv.blog

また、移行時に巨大なpixiv.gitというリポジトリから抜け出したので新たなデプロイフローを構築しました。従来はpployという社内ツールを使いデプロイを行っていましたが、分離して、mainブランチへのpushをトリガーにGitLab CIから自動でデプロイできるようにしました。

現時点で得られたことはありますか?

shu:
狙い通りにデプロイフローが改善しました。従来のデプロイ時に排他ロックをして変更マージしてpployを用いて作業して…といった手動のオペレーションが無くなり、時間を節約できるようになりました。またゼロからコードを移すことで、コア部分を見直したり、レガシーなコードを削減したりすることもでき、開発体験も向上していると思います。

副次的な効果としては、SSRによりCWVのスコアが大きく向上しました。主にLCPが向上しており、ページの最初の表示も体感でも早くなったように感じます。一方でサーバ側での処理が増えたため、TTFBは少し悪化しましたが、総合的には良い結果となりました。

Next.js化前後のLightHouseパフォーマンス測定結果(デスクトップ)

mog:
また、SSRへの移行と併せて前述した「charcoal」も積極的に取り入れたことで、社内のデザインシステム実装のSSR対応などを大きく進展させることができました。

今後について教えて下さい

namazu:
pixivのNext.js化に2022年3月末頃から取り組み、現時点ではpixivのリクエスト機能についてNext.jsでリプレイスすることができました。これはpixivという複雑なウェブアプリケーションのフロントエンドがリプレイス可能なことを示します。また今回pixiv.gitからのフロントエンドコードの分離について手法の一つを確立することもできました。

これからはpixivの他機能についてリプレイスを進めていきたいと考えています。まずは比較的小さいものから手を付け、最終的には作品詳細や検索といった大きな領域を抑えていきたいです。これらは検索流入なども多く、CWVの向上によってユーザー体験が大きく改善できると思うので期待しています。

pixivは機能数も多いのですべて完了するまでは長い話になりますが、丁寧に進めていければと思っています。

終わりに

今回のpixiv insideではピクシブ株式会社で働くエンジニアの取り組みとして、pixivをNext.jsでリプレイスする取り組みについて現場メンバーよりご紹介させていただきました。

ピクシブでは、創作活動を共に盛り上げていただけるエンジニアを積極的に募集しています。今回の記事をご覧いただき興味をお持ちになりましたら、是非エントリーフォームの詳細をご参照ください。エントリーをお待ちしております。

recruit.jobcan.jp