【Unity】ゲーム開発の現場でなぜJenkinsが利用され続けるのか
はじめに
QualiArtsのカレンダー | Advent Calendar 2022 - Qiita、19日目担当の鈴木光です
私はバックエンドエンジニアですが、クライアントのビルド周りも携わるので今回はゲーム開発現場におけるJenkinsについてのお話をさせていただきます
Jenkinsについて
皆さんご存知の通り、Jenkinsは非常に汎用的なCI/CDのプラットフォームです。Jenkins自体はJavaで実装されたOSSのソフトウェアなので基本的に自前でホストする必要がありますが、それゆえに構築してしまえばベンダーロックインされることなく多くの現場で活用することが可能です
最近はGitHub Actionsを筆頭にCircle CI, Gitlab CI, AWS Code BuildやGoogle Cloud Buildなど様々なマネージドCIサービスが業界を席巻していますが、露出の少ないゲーム系のテックカンファレンスなどを見ると未だにJenkinsはよく登場しています。おじいちゃんはまだまだ現役なのです
Jenkinsの欠点
特にWeb系の方々からすると「まだJenkinsなんて使っているの?」という感想ではないでしょうか。かくいう自分もその一人で、入社した時はほうぼうでJenkinsが利用されていて驚いた記憶があります。なぜマネージドサービスを利用しないのかと
実際、QualiArtsのバックエンドエンジニアも以前はサーバアプリケーションのCI/CD基盤としてJenkinsを採用していましたが、最近ではほとんど使っていません。もっぱらGitHub ActionsかGoogle Cloud Buildで完結しています(一部CD基盤にArgoCDを利用していたりしますが、CI基盤としてはマネージドサービスがメインです)。この理由は皆様と同じでしょう、Jenkinsにはいくつか欠点があります
- マネージドサービスではないため自前でマシンやソフトウェアを管理する必要がある
- Jenkins自体がプラグインにとても依存する。プラグインのバージョンについていけなかったり、アップデート作業をするとJenkinsやジョブが壊れる。そして直近のアップデートができないのでその次のアップデートもできなくなり化石になっていくという悪循環
- ジョブや設定がコード化されていない。コード化するプラグインを使ったとしてもやはりそこだけで全ては完結しない(そのプラグインのインストールは?)。さらには馴染みのないGroovy
Jenkinsを利用していた時代もこのあたりを解決するためFabric(GitHub - fabric/fabric: Simple, Pythonic remote execution and deployment.)を使ったり、あまりJenkins側で複雑な設定をしなかったりと工夫していましたが、近年のマネージドサービスはこのあたりを解決してくれるため移行しています。またJenkinsは多岐にわたる機能を持ちますが、我々が行いたかったのは自動テストや単純なビルド・デプロイ作業であったため高度な機能が必要とされなかったというのもあるかもしれません
Unityのビルド
さて、QualiArtsでは主にUnityを用いてゲーム開発を行っており、バックエンドとは違ってクライアントでは引き続きJenkinsを利用しています。具体的にどのようなことがJenkinsによって自動化されているのかは1日目の記事(Unity開発の現場でJenkinsがしていることの紹介 | QualiArtsエンジニアブログ)を参照していただくとして、大別すると「アプリのビルド」と「アセットのビルド」の2つに分けられます。他にも細かいジョブはありますが、そちらは実験的にGitHub Actionsへ移行しています。逆に言うと、これらのジョブは移行することが出来ないわけです。なぜでしょうか?詳しく解説していきましょう
2種類の"巨大な"ビルドとキャッシュシステム
アプリのビルド
「アプリのビルド」から解説していきましょう。こちらは単純明快、クライアントのソースコードからアプリケーションをビルドし、ipaやapkといった実行ファイルを出力するジョブです。これがLinuxマシンで稼働する一般的なサーバアプリケーションであればJenkinsは不要だったでしょう。しかし、ここでビルドのターゲットになるのはiOSやAndroidです。特にiOSのビルドにはmacOSを用いてしかビルドできないという制約があります。したがって、この時点でマネージドサービスであったとしてもかなりサービスを選びます。アプリケーションのビルドにはそこそこのマシンパワーも必要ですのでマネージドサービスにすると費用もかさみます。ビルドジョブには手動で行われるものの他にも1時間に1度など定期的に行われるビルドもあるため、1日のうちそこそこの時間マシンリソースが専有されているというのも従量課金制と相性が悪いです
このため、QualiArtsではMac Proを社内にホストしてJenkinsエージェントをインストール、クラウドのVM上に構築されたJenkinsコントローラーと接続してジョブはMac Pro上で実行するという形式をとっています
アセットのビルド
Unityには画像や動画といったアセットを外部へ切り出すためにアセットバンドルというシステムがあります。Unityがアセットを読み込むためにプラットフォームごとの変換処理やアセットの圧縮などを事前に行い、アセットをメタデータと一緒に外へ切り出すことで、ランタイムで動くアプリケーションに対して動的にコンテンツを配布できるという仕組みです。Unityで開発された運用中のサービスにおいて、新しく追加されるガチャやDLCコンテンツなどがアプリ更新なしに行われているなら十中八九アセットバンドルを使っていることでしょう
このビルド処理にはかなりのマシンリソースが必要で、おろし金と呼ばれる弊社のMac Proを用いても数十分はかかる処理になっています。ただ実はこの処理、ビルドターゲットがiOSでもビルドマシンがmacOSである必要はないのでAWSのスポットインスタンスのようなLinuxのVMを用いてコスト最適化しつつクラウド化することは可能です(過去にそのようなプロジェクトは実在しました)
JenkinsのメリットとUnityビルドにおけるキャッシュ
今までの話だけだと「Macが使えるマネージドサービスならコストを一定かければJenkinsじゃなくて良いの?」ということになるかもしれません。アセットのビルドなんて「macOSじゃくても良いならJenkinsである必要ある?」となることでしょう。しかし、なかなかそういうわけにもいかないのです
先程、Jenkinsのデメリットについてお話しました。逆に我々がJenkins使い続ける究極のメリットを紹介しましょう。それが"ジョブごとにワークスペースが維持される"というものです
一般的に、CIは毎回クリーンな環境で行われる方が良いとされています。そうしないと冪等性や再現性が担保されないためです。変に以前の状態が維持されている環境でCIを実行してしまったがために、想定していたプログラミング言語のバージョンやライブラリの依存関係がおかしくなって挙動が変わるということは容易に想像できます。ただ、これは毎回クリーンな環境で実行したCIが現実的な時間内で終了することが前提です。1テストするのに10時間かかっていたら開発も進みません。しかし、上記2つのビルドではそれが起こるのです
ここでアプリのビルドについて触れてみます。Unityには Library
というディレクトリがあり、色々なキャッシュが詰まっています。これがあることでエンジニアは快適な開発体験を享受できます。 Library
ディレクトリは数GBあり、Unityは Library
ディレクトリ自体やその中のキャッシュデータが不足しているとそれを起動時などに生成しますが、数GBのキャッシュデータを生成するのですから当然時間がかかります。さらにUnityのプロジェクトは大きくなりがちです。VCSからチェックアウトするだけでもかなりの時間がかかります。さらにさらに、アプリのビルドに必要な埋め込みのアセットをSVN, Git LFS, Plastic SCM, Perforceといったアセット用VCSからチェックアウトすることも必要です。これらの処理を毎回するのでしょうか。帯域の問題もありますが、何より時間がかかります。開発中のアプリのビルドは速ければ速いほど検証やQAといったイテレーションを回せるのでビルド時間というのはとても重要な項目です
また、多くのマネージドCIサービスではジョブが並列に走ることが前提です。サービス上にキャッシュの仕組みが用意されているとはいえ、バージョン管理されたコードを並列にビルドしつつ、それを適切にキャッシングするのはなかなか骨が折れるのではないでしょうか。Jenkinsはエージェントごとに並列数も絞れますし、そもそもジョブはキューイングされるので直列のビルドです。以前のジョブで使われていたワークスペースはそのまま維持され、次のジョブでも利用されるため Library
ディレクトリの生成やデータのチェックアウト処理は短時間で完了します
これがそのままアセットのビルドにも関わります。アセット用VCSからチェックアウトする必要があるといったことを先程お伝えしましたが、アプリ埋め込みのアセット量などたかが知れています。本当にヤバいのはアセットバンドルとして外部へ切り出されるリソースたちです。タイトルの規模や対応プラットフォームにもよりますが、例えばQualiArtsのIDOLY PRIDEではこれらは全てのプラットフォーム合わせてデータ量が数十GBあります。アセットバンドルは圧縮されているので生のアセットデータは100GB近くあったりします。この容量になるとマネージドサービスのキャッシュシステムの上限を超えたり、そもそもキャッシュへストア・リストアする作業自体にも時間がかかります。例えばGitHub Actionsでは actions/cache
というワークフローを用いてデータのキャッシュを実現できますが、キャッシュのデータサイズには10GBの上限がありますし、ログを見る感じ内部的には恐らく分散ストレージに対してキャッシュデータをダウンロード・解凍・圧縮・アップロードといった処理をしています。"ジョブごとにワークスペースが維持される"というのはこの問題に対して非常に効果を発揮します。アセットバンドルのフルビルドは本来丸一日レベルのジョブですが、Jenkinsが持つこの特性のおかげで数十分で済んでいます。これがゲーム開発の現場で良くJenkinsが使われている理由です
(ちなみにGitHub Actionsのセルフホステッドランナーはワークスペースを維持しますが、ワークスペースがジョブごとではなくエージェントごとに維持されます。共有ディレクトリをマウントするなどすればジョブごとにすることも可能かもしれませんが、その工夫の維持管理にもコストがかかりますし、どちらにしろ後述する問題があることにより移行は厳しいかなという所感です)
ビルド時間以外のメリット
実はビルド時間以外にもJenkinsにはメリットがあります。それは従量課金でないことと、UIがわかりやすいというものです
我々の現場ではJenkinsのジョブを実行するユーザーがエンジニアであるとは限りません。クリエイターやプランナーなど多様な職種のユーザーがJenkinsのボタンをポチっと押します。仮に素晴らしいキャッシュシステムを構築し、Jenkinsから脱却できるぞと意気込みながらGitHub Actionsに完全移行した場合は、次にこの問題が立ちはだかることでしょう。そう、Jenkinsはユーザー数によってお金がかかるということはありませんが、GitHubはユーザー数による従量課金制なのです。QualiArtsが所属するCyberAgentグループは全社的にGitHub Enterprise Cloudを導入していますが、料金表を見るとユーザーあたり21ドルかかります。しかもこれが日割りだとは思えません。最低でも月ごと、あるいは年ごとになるでしょう。ゲーム開発の現場は人員の流動性も激しく、開発人数も多いです。ソーシャルゲームの開発であっても100人を超える現場は珍しくありません。当然全員がJenkinsを扱うわけではありませんが、必要な人に絞ったとしても利用するなら2週間のインターン生や3ヶ月のヘルプで来ていた人のためにGitHubのアカウントを1ヶ月分・1年分契約したりする必要があるということになります。仮にJenkinsの頃と比べ20人追加でGitHubアカウントが必要になり、それが年契約だとすると5040ドルです。追加でかかるにはちょっと看過できない金額ですね
加えて、Jenkinsは(あまり役に立ちませんが)ジョブの残り時間、進捗やキュー状況などが直感的なUIで表示されます。GitHub Actionsを利用したことがある方ならわかると思いますが、エンジニアにはわかりやすくても非エンジニアにはちょっとわかりにくい作りをしているのではないでしょうか?定期実行されたジョブの確認方法や手動実行するためにはどのボタンを押せば良いかなどの操作方法のドキュメントを書いたり、問い合わせの対応をしたりして工数が発生するならなんのためにGitHub Actionsへ移行したのかわかりません
まとめ
今回はゲーム開発の現場でJenkinsが利用され続ける理由について解説しました。近年では色々な記事やカンファレンスでマネージドCIサービスのことが話され、Jenkinsから脱却したというテーマを聞くことも珍しくありません。ゲーム系でもなんとかしてJenkinsを脱却したという他社さんの取り組みの記事を見ることが増えてきましたが、やはりマネージドMacマシンのスペック不足やキャッシュのための巨大なディスクをネットワークマウントした際に起きる実行速度の問題など課題が残っているという話もされています。バックエンドの人間としてはあまりJenkinsをホストしたくないものですが、現状のゲーム開発現場においてJenkinsは非常に素晴らしいソリューションです。とはいえGitHub Actionsが最適な部分もあるので今後もJenkinsと併用していきたいと思います。もしこのあたりの課題がまるっと解決された、いい感じのCIソリューションがあれば教えていただけると幸いです
【Golang】ジェネリクスのメリデメを比較してみる【1.18】
はじめに
Calendar for QualiArts | Advent Calendar 2021 - Qiita、20日目担当の鈴木光です
今回はGo言語 1.18について導入されるジェネリクスについてのお話をさせていただきます
Go言語 1.18 Betaの公開
先日Go言語 1.18のBetaが公開され、ついにジェネリクスが本格的に使えるようになってきたと話題になりました
go.dev
Go言語は言語仕様にジェネリクスが入っていなかったため、多くの人々はそれを自動生成で賄ってきました
そこでジェネリクスが入ることでどのように実際の開発が変わるのかを見ていきたいと思うのですが、ジェネリクスとは何かや具体的な記法などは多くの方が詳しく解説してくださっているので、今回は具体的にどう変わるのかということに対してスポットを当てていこうと思います
Go言語におけるジェネリクス
とはいえGo言語しか知らない方向けに簡単に説明させていただきます
ジェネリクスとは型パラメータを用いて抽象的なコードを実装することで、特定の型に依存することなく型安全性を維持しながら汎用的なコードを実装する手法のことです
例えばJava言語にはArrayListというリストを表現するための可変長配列のクラスがあります
これは
public class ArrayList<E>
として定義されており、実際には
List list = new ArrayList<String>();
のように使います。ArrayListはListインタフェースを実装しており、型に関わらずadd, remove, sortなどListインタフェースに定義されたあらゆる操作を行うことが可能です
Go言語ではこれを組み込み関数と気合で解決してきましたが、ジェネリクスによって気合が不要になり人類は楽をできるというわけです
実際にGo言語でジェネリクスを使ってみたい方はdevブランチモードのGo Playgroundで簡単に試せます
go.dev
また、こちらがジェネリクスのチュートリアルになります
go.dev
従来の手法による実装
前置きはこのくらいにして実際にどうなるのかを見ていきましょう。ただその前に従来の自動生成による実装を考えてみます
まず皆さんは何をソースに自動生成を行っているでしょうか。私のプロジェクトでは数千ファイルをProtocol Buffersによる定義から出力しているのですが、これはマイナーな手法だと思うので今回はソースをGo言語の構造体で定義しているとしましょう。実運用を考えるとGORMのようなORMのModelからリフレクションなどを用いて情報を取得し、関連コードを生成するのが一般的でしょうか
type User struct { ID uint Name string } type Users []*User
この構造体に対して以下のようなMap, Filter, Findのようなコレクション操作を行いたい場合、どのような実装があるかを考えてみます
func (u Users) Map (f func(u *User) Users) {} func (u Users) Filter (f func(u *User) bool) {} func (u Users) Find (f func(u *User) *User) {}
色々手法はあると思いますが一例を上げてみます
- User構造体とUsers型の定義をuser.goに定義する
- 同一パッケージ内にUsers型を拡張するメソッドを定義したuser_gen.goのようなファイルを生成する
都度実装しても良いのですがそれは面倒です。しかしGo言語にはジェネリクスがありません。そこでメソッドによる構造体拡張を用いることで全てのModel構造体に対してコレクションメソッドを自動生成するという手法です
問題点
さて、この手法にはいくつか問題点があります
まずは自動生成の手間です。新しいテーブルを追加するたび、あるいはコレクション操作が必要になるたびにこの自動生成を実行しなければなりません
次に自動生成コードのメンテナンスです。自動生成するためのスクリプト、テンプレートをメンテナンスしていかなければなりません。ジェネリクスを用いた場合でも当然そのコードはメンテナンスしなければならないのですが、自動生成の場合一つのツールのメンテナンスとなります。メンテナンスコストはジェネリクスのものよりは重くなりがちです
ジェネリクスを導入することによるメリット・デメリット
メリット
ジェネリクスを導入することでその抽象コードのメンテナンスだけで済みますが、型解決のためコンパイル速度は15%ほど遅くなります
Because of changes in the compiler related to supporting generics, the Go 1.18 compile speed can be roughly 15% slower than the Go 1.17 compile speed. The execution time of the compiled code is not affected. We intend to improve the speed of the compiler in Go 1.19.
https://go.dev/blog/go1.18beta1 より
しかし、これは実際には問題とならないケースもあるでしょう。なぜなら大量のコードを生成していた場合、そのコードを読み込むための時間がかかるためです。リフレクションが存在する限り未参照のコードを削除するというようなことはできませんし、そもそも未参照というのを解決するためにもファイルの読み込みが必要だからです
また、大量のファイルが生成されることによるデメリットはいくつか考えられます。セキュリティソフトの起動フックになったり、そもそも自動生成自体がとても長い時間かかったり、IDEのインデックス再構築を待つ時間があったりとコードのメンテナンス以外のデメリットも存在します
デメリット
コンパイル速度についてもそうですが、ジェネリクスによる実装は複雑になりがちだからです。自動生成コードには全てが記述されていますし、Protocol Buffersなどを使う場合はそもそもProtocol Buffers側でそのような抽象化がサポートされいないため、そのあたりの自動生成コードはそのまま使えます
また、自動生成にしかできないこともあります。 sqlboiler が良い例ですが、リレーションされているコードを静的に呼び出すことなど、モノによって実装があるなしが変わるようなものには当然ジェネリクスは使えません。入力された型パラメータによって戻り値が変化するような実装を現在のジェネリクス仕様では表現することができないためです
さらに、専用の関数を直接呼び出すほうが早いということもありえます。以下のコードをご覧ください。なお、実行はできませんのでご了承ください
実行してみたい方は 1.18 beta1系 のDockerコンテナがDocker Hubにアップロードされているので簡単に試すことが可能です。今回は golang:1.18beta1-alpine3.15
イメージを用いてベンチマークを取ってみました。私のPCでの実行結果は次のようなものになっています
/app # go test -bench . -benchmem goos: linux goarch: amd64 pkg: example.com cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz BenchmarkCast-8 11541190 102.3 ns/op 35 B/op 2 allocs/op BenchmarkGenerics-8 11655067 109.7 ns/op 35 B/op 2 allocs/op PASS ok example.com 2.687s /app #
ほぼ変わりませんが、若干ジェネリクス用の実装のほうが遅いことがわかります。重要なのは直接変換関数を呼び出しているか、動的に値の型を取得してtype switchしているかです。型ではなく値の型ということがキモです。本来は型情報だけで最適なswitchへ変換するようコンパイル時に判定できそうですが、現状は cannot use type switch on type parameter value
というエラーが出ており、この記法はまだ未対応なのでこのような結果となっています。これについては下記issueのとおり最適化を待つしか無いようです
https://github.com/golang/go/issues/45380#issuecomment-851580844
まとめ
今回はジェネリクスが導入されることによるメリット、デメリットを比較してみました。この春から始まるプロジェクトについては最初からジェネリクスありきの設計にすることでより柔軟なコードを実装することが出来ると思います。しかし世の中には既に運用中だったり開発中だったり色々なフェーズのプロジェクトが存在し、ジェネリクスを導入するかどうかは一つの大きな決断になると思います。この記事がそのような人々やプロジェクトの参考になれば幸いです
【Golang】自動生成漏れをCIで検知する【GitHub Actions】
はじめに
QualiArts Advent Calendar 2020 - Qiita、7日目担当の鈴木光です
今回はGo言語とGitHub Actionsについてのお話をさせていただきます
自動生成
Go言語は言語仕様上、自動生成によるコード実装が多い言語です
例えば
自動生成したコードは普通にgit commit
しないといけません
ただ、そのうち自動生成コマンド自体の数が増えてくると、コミットの中に漏れが発生することがあります
もちろん、これらが漏れていたからといってアプリケーションが必ずしも動作しないわけではありません
テスト対象によってはmockコードがなくても通る場合はありますし、そもそもORMなどは漏れること自体があまりないでしょう
しかし、自動生成を忘れた人以外の人がそれらを生成した際に、自分の変更範囲外のものまで生成されるとちょっとアレじゃないでしょうか?
今回はその漏れを見逃さないようにGitHub Actionsを用いてチェックする方法をご紹介します
実装
今回は次の3つについて、GitHub Actions上で自動生成が漏れていないかチェックする実装を行っていきます
また、基本的に自動生成が漏れていないかチェックするだけなら
- コードを生成
git diff --quiet
でチェック(diffがある場合は終了コードが1になる)
だけで済みます
ORMのコード&単純なCRUD API
現在sqlboilerという既存DBから実装コードを生成するタイプのORMを利用しています
Go言語だとやはりgormをよく見かけますが、sqlboilerはタイプセーフなコードを生成できてパフォーマンスも良いのでお気に入りです
加えてマスターデータを操作する単純なCRUD APIを自動で生成しており、カラム追加時などにちょくちょく忘れてしまうのでチェックしたいと思います(むしろこちらがメイン)
また、DBマイグレーションのツールとしてdbmateを使っています。非常に使い勝手が良いのでぜひ使ってみてください
name: Check autogen db code on: pull_request: branches: - master - 'feature/**' paths: - 'db/migrations/*' # マイグレーションファイルに変更があった時だけ実行 jobs: check-autogen-db: runs-on: ubuntu-latest services: mysql: image: mysql:5.7 ports: - 3306:3306 env: MYSQL_ROOT_PASSWORD: root steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup golang uses: actions/setup-go@v2 with: go-version: 1.14.10 - name: Cache Go Modules uses: actions/cache@v2.1.3 id: cache with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Download Modules if: steps.cache.outputs.cache-hit != 'true' run: go mod download - name: Migration database uses: docker://amacneil/dbmate:v1.10.0 env: DATABASE_URL: mysql://root:root@mysql:3306/hoge with: args: --wait --no-dump-schema up - name: Setup tools run: GO111MODULE=off go get github.com/volatiletech/sqlboiler github.com/volatiletech/sqlboiler/drivers/sqlboiler-mysql - name: Generate entity run: sqlboiler mysql --wipe - name: Check diff run: git add . && git diff --cached --quiet - name: Show diff if failure if: ${{ failure() }} run: git diff --cached - name: Generate master api run: make gen-masterapi # マスターデータ操作APIを自動生成する独自実装 - name: Check diff run: git add . && git diff --cached --quiet - name: Show diff if failure if: ${{ failure() }} run: git diff --cached
テストで使うmockコード
お次はmockコードの生成です
現在テストライブラリとしてアサーションにtestify、mock生成にmockeryを利用しています
自動生成コードなど一部のファイルをテスト対象から外しているケースでそこへ変更があった際に生成を忘れがちです(そもそも生成対象からはずせよっていう話でもありますがw)
name: Check autogen mock code on: pull_request: branches: - master - 'feature/**' paths: - 'pkg/**' jobs: check-autogen-mock: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Generate mock run: rm -rf mock && docker run -v ${PWD}:/src -w /src vektra/mockery:latest --all --dir=./pkg --keeptree --outpkg=mock --output=mock --disable-version-string # dockerコマンドも使える - name: Check diff run: git add . && git diff --cached --quiet - name: Show diff if failure if: ${{ failure() }} run: git diff --cached
APIドキュメント
最後はAPIドキュメントです
前提として現在のプロジェクトではコードに埋め込んだアノテーションからSwagger Specを生成する手法をとっており、go-swaggerを利用しています
GitHubのリポジトリにSwagger Specを置く運用をしているのですが、APIのリクエスト・レスポンスを更新した時に生成を忘れてしまうなどはよくあるユースケースです
そんな時はGitHub ActionsにSwaggerの生成とドキュメントのコミットまでやってもらいましょう
同一リポジトリ内にコミットするなら非常に簡単なのですが、今回はドキュメント専用の別リポジトリに対してコミットしたかったのでdeploy keysを利用します
こちらのリポジトリにはシークレット情報としてDOCS_REPO_DEPLOY_KEY
という名前で秘密鍵を設定します。ドキュメント側のリポジトリには公開鍵を登録しておきましょう
name: Generate Swagger Spec on: push: branches: - master paths: - 'pkg/presentation/http/router.go' - 'pkg/presentation/http/request/**' - 'pkg/presentation/http/response/**' jobs: generate-swagger: runs-on: ubuntu-latest env: DOCKER_WORKDIR: /github/workspace SPEC_PATH: spec/swagger.yaml steps: - name: Checkout code uses: actions/checkout@v2 - name: Swagger specを生成 uses: docker://quay.io/goswagger/swagger:latest with: args: generate spec -m -o ${{env.DOCKER_WORKDIR}}/${{env.SPEC_PATH}} - name: deploy keyをセットアップ env: DOCS_REPO_DEPLOY_KEY: ${{secrets.DOCS_REPO_DEPLOY_KEY}} run: | echo "$DOCS_REPO_DEPLOY_KEY" > ~/deploy_key.pem chmod 600 ~/deploy_key.pem - name: diffがあればdocs用リポジトリにcommit&push env: GIT_SSH_COMMAND: ssh -i ~/deploy_key.pem -o StrictHostKeyChecking=no -F /dev/null DOCS_REPO: hoge-server-docs run: | git clone git@github.com:hikyaru-suzuki/$DOCS_REPO.git ../$DOCS_REPO cd ../$DOCS_REPO mkdir -p ./spec rm -f ./${{env.SPEC_PATH}} cp $GITHUB_WORKSPACE/${{env.SPEC_PATH}} ./${{env.SPEC_PATH}} git add ./${{env.SPEC_PATH}} result=0 $(git diff --cached --quiet) || result=$? if [ $result -ne 0 ]; then git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git commit -m "update: swagger spec" git push origin master fi
まとめ
小さなことですが今回のCI設定で楽になりました
個別の作業はそれぞれ1分もかからないものですが、自動生成の確認は毎回しないといけないので地味にストレスになっていたのかもしれません
自動生成自体はGo言語に限ったことではないですし、何かの参考になれば幸いです
参考
PHPの各種フレームワークにおけるDIについて
QualiArts Advent Calendar 2019、2日目担当の鈴木光です。
タイトルの通り、今回はPHPの各種フレームワークにおけるDIについて書いていこうと思います。
Dependency Injection(DI)
あるクラスが依存するオブジェクトをコンストラクタ、メソッド、セッターなどの引数から注入することでクラスから依存関係を減らすという手法です。テストや仕様変更に強くなるという特徴があり、10年以上前から用いられてきたデザインパターンとなります。
ちなみにDIの説明のためにこちらの記事をとても参考にさせていただきました。
DIとは
DI自体はそんなに難しいことではありません。以下のようにコンストラクタなど対して依存するオブジェクトを注入すればそれはDIパターンを適用している(DIしている)と言えます。
<?php class UserController { private $userService; public function __construct(UserServiceInterface $userService) { $this->userService = $userService; } }
DIコンテナ
DIコンテナとはDIする際に利用するインスタンスが登録されたオブジェクトです。このDIコンテナの仕様はPSR(PHP Standards Recommendations = PHP標準勧告)のPSR-11で定義されており、Psr\Container\ContainerInterface
を継承することで簡単に実装することが可能です。
DIコンテナを使わない場合はnew UserController(new UserService())
とするところを、new UserController($container[UserService::class])
のようになります。このDIコンテナにインスタンス生成のロジックを集約しようという手法です。
ここで注意していただきたいのはDIするのにDIコンテナは必須ではないというところです。なので次の話に繋がります。
ちなみにDIコンテナの実装をサポートするライブラリとして以下のようなものになります。
- PSR-11: Container interface - PHP-FIG
- Pimple - A simple PHP Dependency Injection Container
- GitHub - Level-2/Dice: Dice - a lightweight Dependency Injection Container for PHP
- GitHub - AcclimateContainer/acclimate-container: Adapters for PHP framework containers to an interoperable interface
- GitHub - ray-di/Ray.Di: Guice style dependency injection framework for PHP
- PHP-DI - The Dependency Injection Container for humans
サービスロケータ
DIコンテナを使うとこういうことも出来てしまいます。
<?php class UserController { private $userService; public function __construct(ContainerInterface $container) { $this->userService = $container[UserService::class]; } }
これがサービスロケータと呼ばれます。アプリケーションは$container
を通じて任意のインスタンスを取り出すことが出来ます。これの問題点はUserControllerがUserServiceではなくDIコンテナに依存していることです。従ってサービスロケータでは依存するオブジェクトが格段に増えています。
しかしUserControllerでは本当にDIコンテナに登録されている全オブジェクトが必要なのでしょうか。この場合だとUserServiceだけで大丈夫のはずです。しかし注入する側はそんなこと知りません。なので中身を知らない場合は完全に実装されたDIコンテナを注入する必要があります。これはテストの実行速度や保守性に関わってきます。
このことからサービスロケータはアンチパターンとされています。
PHP各種フレームワークにおけるDI
Symfony
みんな大好きSymfonyです。PHPのフレームワークなどで最初に行うcomposer install
を眺めていたらだいたい出てきます。
Symfonyではサービスコンテナを利用します。これはサービスクラスが登録されたDIコンテナです。
また、Symfony ComponentとしてSymfony以外のアプリケーションではsymfony/dependency-injection
コンポーネントとして利用出来ます。Symfonyアプリケーションでは前述のサービスコンテナという仕組みを利用します。使い方は次のとおりです。
config/services.yaml
で設定ファイルを書く(自動構築されたプロジェクトならデフォルトで書かれている)- コンストラクタやメソッドの引数にサービスコンテナに登録されたインタフェースの型で変数を定義
以上です。これだけでインスタンスがオブジェクトへ注入されます。まるで魔法のようですが、割と他のフレームワークやライブラリでも同じようなことが出来るようになっています。実際はリフレクションを使用して型を検出し定義ファイルと突き合わせて注入しています。
自動で依存関係を作成し注入することをAutowireやAutowireingと呼び、型を検出して注入することをPHPでは型宣言のことをタイプヒントと呼ぶことから、これをタイプヒントやタイプヒンティング(typehint or typehinting)と呼びます。リフレクションを使用するとパフォーマンスが低下するため、複雑なDIライブラリでは事前コンパイルをするなどしてパフォーマンスを向上させているようです。ちなみにアノテーションで注入するライブラリもあります。
Zend Framework
- GitHub - zendframework/zendframework: Official Zend Framework repository
- Manual - Documentation - Zend Framework
- Manual - Documentation - Zend Framework
- Manual - Documentation - Zend Framework
こちらもみんな大好きZend。composer install
で見ることが多いですね。
ZendではPSR-11準拠のDIコンテナを利用します。また、InstanceManager :: addTypePreference()
というインスタンスメソッドを呼び出して設定することでAutowiringも可能となります。さらにZend\Di Definition
を定義してリフレクションにかかる時間を短縮することも出来ます。
Laravel
最近流行りのLaravelです。Laravelでもサービスコンテナを利用します。こちらは注入する方式としてサービスプロバイダを利用します。使い方は次のとおりです
app/Providers/
配下に新規にServiceProviderを実装したクラスを作る or デフォルトで用意されているAppServiceProviderに実装を記述- 新規でProviderを作成した場合、
config/app.php
のprovidersマップに対象クラスをclassキーワード(::class
)でクラス名解決を行い注入するサービスを設定
wiringする仕組みとしてサービスプロバイダを使っているのですね。これによりAutowiringが実現します。また、サービスプロバイダの中でエイリアスとオブジェクトを結合(登録)するわけですが、それにもいろいろな結合方法があります。
\Illuminate\Contracts\Foundation\Application::bind()
の第2引数のクロージャでnewして結合\Illuminate\Contracts\Foundation\Application::singleton()
を使ってシングルトンとして結合\Illuminate\Contracts\Foundation\Application::instance()
でnewしたインスタンスを結合\Illuminate\Contracts\Foundation\Application::when()->needs()->give()
でプリミティブな値を結合
ちなみにLaravelでは依存オブジェクトを注入しなくても、オブジェクトのメソッドをFacadeという仕組みでクラスメソッドとして呼び出すことができます(なんかDIの意味が薄れてきますが)。
CakePHP
RailsのPHPクローンとして一世を風靡したCakePHPさんです。
CakePHPではサービスロケータとして依存解決を行います。このDIコンテナ流れの中、頑なにCakePHPはサービスロケータで生きています。ちなみにまだかなり先ですが4.1のロードマップにPSR-11準拠のDIコンテナの実験的サポートとありますので気長に待っていればそのうち来るかもしれません。
4.1 Roadmap · cakephp/cakephp Wiki · GitHub
例えばCakePHPではORMのテーブルクラスを取得するために次のように実装します。
TableRegistry::getTableLocator()->get('Articles');
use Cake\ORM\Locator\LocatorAwareTrait;
したクラスで$this->getTableLocator()->get('Articles');
CodeIgniter
Phalconが出るまで最速の地位にあったCodeIgniterではフレームワーク側でDIの仕組みは用意されていません。したがって自身で実装する必要があります。
Slim
- GitHub - slimphp/Slim: Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
- Dependency Container - Slim Framework
PHPのマイクロフレームワークです。SlimにはビルドインでPimpleベースのDIコンテナが実装されており、依存解決はこちらを利用します。
SlimのDIコンテナは非常にシンプルな実装で、結合も単純明快です。わかりやすいのでここにも記します。
<?php $container = new \Slim\Container; $app = new \Slim\App($container); $container = $app->getContainer(); $container['myService'] = function ($container) { $myService = new MyService(); return $myService; }; $app->get('/foo', function ($req, $res, $args) { $myService = $this->get('myService'); return $res; });
また、\Slim\App::get()
などのクロージャ中で$this->myService;
のように暗黙的にサービスを取得できます。また、サービスが登録されているかどうかは$this->has('myService')
として確認できます。
Yii 2
- GitHub - yiisoft/yii2: Yii 2: The Fast, Secure and Professional PHP Framework
- 鍵となる概念: 依存注入コンテナ | Yii 2.0 決定版ガイド | Yii PHP Framework
- 鍵となる概念: 構成情報 | Yii 2.0 決定版ガイド | Yii PHP Framework
- 鍵となる概念: サービス・ロケータ | Yii 2.0 決定版ガイド | Yii PHP Framework
Yiiにも独自のDIコンテナが存在します。そしてYiiはDIコンテナ上にサービスロケータを実装されており、アプリケーションはそのコンストラクタやメソッドに対して直接オブジェクトを注入するかコンテナを注入することでそのどちらで依存解決を行うか選ぶことが出来ます。
コンテナへの登録はyii\di\Container::set()
で行うこと出来ます。複数のサービスを登録するにはyii\di\Container:: setSingletons()
やyii\di\Container:: setDefinitions()
を利用します。ちなみにアプリケーション構成ファイルを用いて設定することも出来ます。
Phalcon
- GitHub - phalcon/cphalcon: High performance, full-stack PHP framework delivered as a C extension.
- 依存性の注入とサービス・ロケーション — Phalcon 3.0.2 ドキュメント (Japanese / 日本語)
ハイパフォーマンスなフルスタックフレームワークであるPhalconではPhalcon\Di
というDIコンテナが実装されています。このインスタンスに対して$di->set()
で登録を行い、$di->getService("logger")
でコンテナから取り出します。シングルトンとして登録したい場合はPhalcon\Di::set()
の第3引数にtrueを設定することでシングルトンになります。
まとめ
いろいろなフレームワークにおけるDIを見てきました。最初はただ依存するオブジェクトを外出ししてnewするタイミングで注入するだけだったのが、フレームワークやライブラリ上にアプリケーションが構築されることを前提とすることでリフレクションやアノテーションを用いて注入されることが多いです。
最近、弊社でもGo言語の機運が高まってきており、Goではそのような注入ではなく自動生成ツールを用いて注入するコードを生成してそれを使ったりします(https://github.com/google/wire)。
自動的に注入されるのは確かに便利ですがこれは宣言的ではありません。最近のPHP7系やPython3.5系のように明示的な型宣言(typehint)、Goのようにコードを積極的に生成してフレームワークはあまり用いず読めばわかるをモットーに保守性を上げていくことが今後の流れなのかもしれません。
Immutableなデータベースの紹介
はじめに
これはCyberAgent 19新卒 エンジニア Advent Calendar 2018の2日目の記事です。
これが初エントリーなので自己紹介を。19卒で株式会社CyberAgentに入社する予定の鈴木光です。現在は株式会社CAmotionというスタートアップでサーバサイドエンジニアとして働いています。別のアルバイト先では設計からフロントまでやってます。
技術的にはPHPとRDB設計がメインで最近はVue.jsやGoもやっています。ER図大好き。
趣味ではUnityやUE4でゲームを作ったり、MMDで動画作ったり色々やってます。ちなみにお菓子作りが好きでシフォンケーキを焼くのが得意です。
続きを読む