シンプルなNodeアプリをDocker化してElastic Beanstalkに移行してみた
DBも使わない超シンプルなnodeアプリをHerokuで管理していたが、 いつぞやからか無料枠では月終わりにスリープするように体系がかわってしまったので移行を考えていた。
とりあえず勉強もかねて、AWSでDocker化してみることに。
今回のゴール
- HerokuのHobbyプラン7$/月よりも低コストでホスティングする
- その代わり最低限のスペック・冗長化でよい
- 基本的なDockerのサービス使ってみることで使いまわせる枠組みを得る
- ミニマムなデモアプリでの利用
- 開発環境構築とワークフローの汎用化
- Herokuレベルな触りごごちで簡単にデプロイ・管理できるようにしておく
- HTTPS化する
AWS上のサービスや構成の検討
とりあえず、どんな構成で行くかをちょっと使ってみたりして比較
今回はコストが最優先。 なので、インスタンスはt3.nano1台くらいでいきたい。 ロードバランサーはいらない。お遊びでやるには高い(立てとくだけで月2000円くらいか)。
これじゃDockerのメリットなくないって? 現状はあまり無いが、負荷が増えた時にスケーリングできる構成に柔軟に変えられる点がいいかも。
次はどんなサービスを使うか。以下検討
- Fargate
- ECS
- 細かく設定をできそうだが、その反面自前でいろいろやる必要が出てきそう。今回は生で扱うのはオーバースペック感。特に構成管理やデプロイフロー周りをしっかり検討する必要がありそう。
- EKS
- 未調査。Kubernetesは今後のメインストリームに?今回はまずシンプルな構成の勉強も兼ねるので一旦考慮外
- Elastic Beanstalk
- マルチコンテナで利用する場合ECS上で動作するが、その管理あたりを丸っと任せられる。デプロイも容易。
ということで、今回つかうもの&ざっくり構成。
- 複雑なことしない+管理を楽にしたいのでElastic Beanstalkを使う。
- せっかく&独自イメージをプライベートなところに置きたいのでECR(Elastic Container Registry)もつかう。
- シングルインスタンスでロードバランサーは使わない。
- LBなしでSSL化に伴う証明書の管理がAWS上でできないので、インスタンス内にNginxのコンテナを立てそこに証明書を配置する。
- ドメインはお名前.comで取得しているが、こちらもせっかくなのでRoute53のDNSサービスを利用することに。
Local Docker Install
そもそもMacを新調してローカルにDockerが入っていなかったので、以下を参考にインストール。 登録とかめんどかったのでbrew経由でいれた。
$ brew install docker $ brew cask install docker
Node App コンテナ化
まずは、アプリ自体をコンテナ化。 いろいろ検討した結果、すこし古い記事だが、基本が抑えられていたので以下を参考にDockerfileや開発フローを構築。 postd.cc
ベースとなるImageは設定の楽さとサイズの兼ね合いでnodeのslimを選択
これでも220MBくらいになるんで本当はもっと最適化した方が良さそうだが。
以下のようなDockerfileを構築
また、Imageのbuild時にアプリケーションのソースを丸っとコピーするが、 モジュールのインストールはbuild時に行うのでnode_moduleはコピーの対象外にしておく。 (ローカルでnpm installしてた場合とか、問題は無いだろうが冗長なので)
.dockerignoreを作成
node_modules npm-debug.log
この状態でも、 docker buildしてdocker runすればコンテナが起動してアプケーションに接続できる。
SSL化の準備・ローカルでのチェック環境
とりあえずSSL化のことについてちょっと考える。 まず証明書は無料で発行したい。ただALBは使わないので、とても楽なCertificate Managerとの連携がいまいち。
そこで以下を参考にhttps-portalを利用してみることにした。 qiita.com
基本はnginxのプロキシWebサーバーだが、Let's encryptから自動で証明書を取得・更新をしてくれる。 デモレベルのアプリであれば十分そう(自前で管理できないのはやりにくい面もあるが)
まずは、ローカルでhttps接続を確かめられる環境としてdocker-composeファイルをつくる。 ちなみに本番デプロイ前の確認用ということでdocker-compose.stg.ymlという名前にしている。
Let's encryptは証明の発行上限が週ごとにあるので、開発段階やチェック等で本番接続していると動かなくなる。 そこでenvironmentのSTAGEをlocalにしておく。
こうしておくと、オレオレ証明書を自前で発行して代わりに使ってくれる。
またローカルでhttps接続のチェック様にhostsを修正しておく
sudo vi /etc/hosts ... 127.0.0.1 mikinya.net.localhost ...
これでとりあえず、以下コマンドでImageのビルドとコンテナの起動ができる。
$ docker-compose -f docker-compose.stg.yml build $ docker-compose -f docker-compose.stg.yml up
でブラウザからこのローカル用のドメインにアクセスした時に、https-portalとcontainerの連携のチェックができる。
開発環境・開発フロー
現状のままだとソースコードを編集するたびにdocker-compose buildしないといけないのでめんどい。 開発用のフローを別途用意する。
そもそも、node起動後ファイル更新検知・リロードが自動でできていなかったので、nodemonを導入した。 package.jsonのdependenciesにnodemonを追加
... "dependencies": { "nodemon": "^1.18.9", ...
そして、普段づかいのローカル開発環境用docker-composeを別途つくる。
こっちのポイントは以下
- ローカル開発環境ではnginxのプロキシを利用しないでシンプルに単体で立てる
- 起動コマンドを上書き、nodemonが起動する様にする
- gitリポジトリローカルにあるソースをvolumesとしてマウントし、ローカルのソースを直接起動・コンテナ内の起動アプリをすぐに更新する
- mode_modulesのvolumeを別途作成し、コンテナ内の領域からインストールしたmode_modulesを参照する
これで、package.jsonを更新後、コンテナ内からpackage-lock.jsonの更新が可能で、ローカルのリポジトリにコミットできる。しかもnode_modulesは開発環境では動的に更新が可能で、imageにビルドするときはpackage.json、package-lock.jsonできちんとインストールされる。
詳細はこちらを参考に
DockerでのNodeアプリ構築で学んだこと | POSTD
なお、Macだとファイルシステムの都合上、動作がめちゃくちゃ遅くなるという問題があるみたいだが、アプリケーションが小さいせいか、実用上全く気にならなかったのでそのままにしている。
もし問題がおきたら、docker-sync等を使うのが良いのかも。
まぁ、ひとまず、開発開始の流れがめっちゃ簡単になった。
以上、これで、locahostに開発環境が立ってソースの編集もできる。 npmもnode入れる必要もないし、npm installもいらない。楽
AWS準備
awsコマンドもインストールしていなかったのでインストール
brew install awscli
aws configureで適切な権限を持つユーザのAccess Key とかSecretとか入れる
また、Elastic Beanstalkのマルチコンテナを使う場合は何かしらのコンテナのレジストリサービスが必要。
シングルコンテナの場合はソースコードをアップロード後にbuildするみたいだが、 今回はNginxのコンテナもフロントに立てたいのでマルチコンテナでECRをつかってみる。
ECR
あらかじめECRのコンソールから適当な名前をつけてリポジトリを作成。
また、ゴミがたまるのも嫌だったのでLifecycle Policyも設定。
これで3つ以上のタグが無いImageは消えるかな?(たぶん)
でローカルのビルドとpushが面倒だったのでスクリプトを作成(scripts/push-image.shに配置)。
Elastic Beanstalk準備
Elastic Beanstalkのコンソールから新しいアプリを作成。
プラットフォームをMulti Container Dockerにしておく。 とりあえずサンプルのアプリをデプロイする設定で、ロードバランサーなし、他はだいたいデフォルト。
構築が終わり提示されたURLにアクセスするとNginxが立っている様が見れる。 EC2に指定したインスタンスが立ち、EIPが振られ、ECSに項目として現れる。
Dockerrunファイル作成
Dockerrunファイルをつくる。基本docker-compose.ymlを単純に置き換える感じ。
ただ、独自にbuildしたアプリのimageの取得先がECRのURLになり、https-portalの証明書の取得を本番環境設定してやる必要がある。
Elastic Beanstalkロール権限の設定
Elastic Beanstalkの管理下でインスタンスからECRにアクセスしImageを取得できるようにしないといけない。
Elastic Beanstalkを利用し始めると、IAMにaws-elasticbeanstalk-ec2-roleが追加されているので、 こいつにAmazonEC2ContainerRegistryReadOnlyポリシーをアタッチする。
Route53設定
https-portalを利用していることもあって、おそらくDNS設定をしてからじゃ無いと証明書取得がこける気がする(未確認)。まぁ大したアプリじゃ無いので、一旦停止を良しとしDNSを最初に切り替えておく。
Route53にて新たに取得したDomainのCreate Hosted Zoneを作成。さらにCreate Record SetからRecordを追加
- Type: A
- Alias: Yes
- Alias Target: Elastic Beanstalkで作成したアプリケーションURL
あとはお名前.comのDNS設定にNSレコードの情報を追加
セキュリティーグループ設定
Elastic Beanstalkがインスタンスに設定したセキュリティグループにのインバウンドにHTTPS/443を追加
Elastic Beanstalkデプロイ
さて、あとはデプロイ。基本的にコマンド一つでデプロイしたい。基本はebコマンドでできる。
ebコマンドをインストール&初期設定
brew install awsebcli eb init
ただデフォルトのeb deployは、gitのアーカイブをアップロードする。ImageはすでにECRにpush済みなので、リポジトリのソースファイルはデプロイに必要ない。
大した容量にはならないが、必要なのはDockerrun.aws.jsonだけなので冗長なものはアプロードしたくない(s3にたまる)。
調べてみるとebコマンドはデプロイするものを任意指定でき、独自にデプロイのフローを制御できるらしい。ということで、自前でDockerrun.aws.jsonをzip圧縮してそれだけあげることに。
まずは、.elasticbeanstalk/config.ymlに以下を追加する
deploy: artifact: .elasticbeanstalk/artifact.zip
これでeb deployコマンドで.elasticbeanstalk/artifact.zipがアップロードされるようになる。
さらに独自のdeployスクリプトを用意する(scripts/deploy.shに配置)。
#!/bin/bash set -eu zip .elasticbeanstalk/artifact.zip Dockerrun.aws.json eb deploy
zip圧縮後deployコマンドを叩くかんじ。
ただ現状のデプロイフローはまだ問題がある
- ERCへのpushスクリプトとdeployスクリプトを二回叩く必要がある
- .elasticbeanstalk/config.ymlがignoreされているのでartifactの設定を他の端末でやり直す必要がある
- gitのリビジョンとデプロイが紐づかない(Dockerrun.aws.jsonはコミット前でもデプロイできちゃう)
もろもろ考えると、さっき作ったimageをビルドしてECRにあげるスクリプトもデプロイスクリプトに組みこむといいのかも。 また、独自zipをつくるより.ebignoreファイルを作って、Dockerrun.aws.json以外は無視するほうがよいのかも。
開発とデプロイのまとめ
- ローカル開発
- docker-compose up
- ブラウザからlocalhost:3000にアクセス
- ファイルはそのまま編集して、コミットすればよい
- Webフロントサーバー込みのローカルチェック
- docker-compose -f docker-compose.stg.yml build
- docker-compose -f docker-compose.stg.yml up
- ブラウザからhttps://mydomain.localhostにアクセス
- 本番デプロイ
- scripts/push-image.sh
- scripts/deploy.sh
目標コストに関して
さて、そんなこんなで、AWS上にアプリを移行しましたが、コストを計算。 ちなみに東京リージョンです。
- EC2 : 0.0068USD/Hour * 24 * 30 = 4.896USD
- EBS : 0.12USD/GB/Month (gp2) * (12 + 8)GB= 2.4USD
- Route 53 : 0.5USD/HZ = 0.5USD
- ECR : 0.10USD/GB/Month * 0.1GB = 0.01USD
- EIP : 0USD
- ECS : 0USD
合計:7.806USD
おっと...細かい費用がかさんで目標のHeroku Hobbyプラン7$より高くなってしまった...w
まぁあとは、 リージョンをオレゴンにするともう少し安くて、6.726USDでいけそう。 あとはリザーブドインスタンスを使うってとこかな。
もう少し研究してみます。
Zenject InterfaceのBindにおけるIdの付け方
BindするInterfaceにIdをつける場合WithIdの書き方が限定される。 BindInterfacesAndSelfTo等を利用してまとめての指定はできなそう。
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { // できない Container.BindInterfacesTo<Sample>() .WithId(1) .AsCached(); // できない Container.BindInterfacesAndSelfTo<Sample>() .WithId(1) .AsCached(); // できる Container.Bind<ISample>() .WithId(1) .To<Sample>() .AsCached(); } }
Zenjectとかが絡むコンポーネントのUnit Test
Zenjectが絡んだときのUnit Testの例を紹介します。 あくまで例です。
例えば以下のようなHomeUseCaseというクラスのテストを行う場合を考えます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UniRx; using Zenject; using System; public interface IHomeUseCase : IUseCase { IObservable<IHomeViewModel> LoadHomeModel(); } public class HomeUseCase : IHomeUseCase { [Inject] IUserRepository userRepository; public IObservable<IHomeViewModel> LoadHomeModel() { return userRepository.Get() .Select(entity => new HomeViewModel(entity)); } }
HomeUserCaseはLoadHomeModel()をコールすると、 いろいろやってIHomeViewModelというインターフェースを取得するためのObservableを返却します。
また、HomeUseCaseはIUserRepositoryに依存しており、Injectアトリビュートで依存注入されています。
ちなみにUserRepositoryは以下のようなIOを担当するようなコンポーネントで、 通常はサーバー通信を行うコンポーネントとのインターフェースになります。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UniRx; using Zenject; using System; public interface IUserRepository : IRepository { IObservable<UserEntity> Get(); } public class UserRepository : IUserRepository { [Inject] IUserDataStore dataStore; public IObservable<UserEntity> Get() { return dataStore.Get(); } }
この時点でHomeUseCaseをテストするためには以下の要点をどうにかする必要があります。
- TestコードでInjectアトリビュートやContainerをどう扱うか
- UserRepositoryをそのまま使うとサーバーへ通信してしまうし、そもそもHomeUseCaseのテストにフォーカスしたい
- UniRx使っていて非同期ぽいのをどうテストするか
解決方針
こんな状態のコードをテストをするために以下の2つの方針をたてます。
ZenjectUnitTestFixtureを使う
「1.」についてはZenjectのOptionalExtras以下にあるZenjectUnitTestFixtureを継承したクラスでテストを行うことで解決できます。 ZenjectUnitTestFixture継承したクラスでは、デフォルトでContainerを扱うことができ、 そこにテストに必要なコンポーネントをバインドしていくことでInjectアトリビュート等を扱ったテストが可能になります。
Mockフレームワークを使う
「2. 3.」に関してはMockを利用します。 Mockは本物のオブジェクトの代わりにテスト用のオブジェクトに差し替える仕組みです。 これにより、依存するオブジェクトをすべて用意する必要がなくなり、 テストしたいものだけにフォーカスできます。 また、依存するオブジェクトの状態も自由に用意できます。
ZenejctにはOptionalExtras以下にMockフレームワークのMoqが付属しているのでzipファイルを解答することで利用可能になります。
OptionalExtras以下にあるAutoMockingをダブルクリック後に展開されるファイルの中に Moq-Net35、Moq-Net46があるので、Unity Editorの状態に合わせて再度ダブルクリックします。 するとMoqディレクトリが展開されます。
実際のテストコード
このあたりを含め以下のようなテストコードを実装してみました。
using UnityEngine; using UnityEngine.TestTools; using NUnit.Framework; using System.Collections; using Zenject; using UniRx; using Moq; public class HomeUseCaseTest : ZenjectUnitTestFixture // ① { [Inject] HomeUseCase useCase; // ⑥ [Inject] Mock<IUserRepository> mockRepository; // ⑥ [SetUp] public void CommonInstall() { Container.Bind<HomeUseCase>().AsSingle(); // ② var mockRepository = new Mock<IUserRepository>(); // ③ var mockDataObservable = Observable.Return<UserEntity>(new UserEntity()); mockRepository.Setup(x => x.Get()).Returns(mockDataObservable); Container.BindInstance(mockRepository.Object); // ④ Container.BindInstance(mockRepository).AsSingle(); // ⑤ Container.Inject(this); // ⑥ } [Test] public void CallRepositoryAndReturnsHomeViewModel() { IHomeViewModel viewModel = null; useCase.LoadHomeModel() .Subscribe(vm => viewModel = vm); // ⑦ Assert.IsNotNull(viewModel); mockRepository.Verify(x => x.Get(), Times.Once()); // ⑧ } }
説明箇所ありすぎなので番号を振っています
①ZenjectUnitTestFixtureの継承
テストコードはZenjectUnitTestFixtureクラスを継承します。 これでContainer等の扱いが可能になり(Baseクラスで定義)、Zenjectのテストがしやすくなります。
②CommonInstall内でのContainerへのBind
テストに必要なコンポーネントはContainerへBindしていきます。 まずはテスト対象のHomeUseCaseをバインドしています。 CommonInstallはSetupアトリビュートによりテストの前に実行されます。
③依存コンポーネントのMock生成
通常のZenjectのContainerと同様、依存するコンポーネントはContainerの中にBindしておく必要があります。 テスト対象のHomeUseCaseはIUserRepositoryに依存しているためにどうにか用意しBindします。
しかしUserRepository自体をBindすると以下の問題がでます。
- UserRepositoryが依存しているクラスもBindする必要がある
- HomeUseCaseのテストなのにUserRepositoryの状態に深く影響される
ということで、ここでMockを利用しています。 Mockを利用するためには対象のクラスにインターフェース定義が必要です。
ここではIUserRepositoryが定義されており、 HomeUseCaseでもすべてインターフェースで取り回ししているため、Mockでのテストが可能になります。
Moqの詳しい利用法は省略しますが、新しいMockオブジェクトを生成し、 Get()メソッドをスタブし、「即時UserEntityを返却するObservable」を返却するようにしています。
④MockUserRepositoryのBind
MockオブジェクトのObjectプロパティをBindすることで、Mockオブジェクトのインターフェースをbindできます
⑤Mockオブジェクト自体のBind
今回は振る舞いに関してもテストしたかったので(あとで解説)モックオブジェクト自体もBindしています
⑥テストクラス自体のBind、テスト対象のInject
最後にテストクラス自体もBindしてます。 これはちょっとしたテストコードで楽をするための工夫で、 こうすることにより、 冒頭に宣言したInjectアトリビュートのプロパティにContainer内のテスト対象のクラスがInejctされ、そのまま利用できるようになります。
⑦実際のテスト
LoadHomeModelメソッドを実際にコールし取得したObservableを稼働させることでテストを実行します。 ③で本来は非同期のObsevableを即時値を返すように設定しているので、 Subscribeをコールした直後、OnNextのストリームが流れSubscribe内のラムダに処理が移ります。 ここでIHomeViewModelがちゃんと返却されるかどうかをテストしています。
⑧UseCaseの振る舞いのテスト
一応、UseCase内でちゃんとUserRepositoryを利用しているかをチェックしています。 やろうと思えばUserRepositoryを経由せず、内部でクラスをインスタンス化しても⑦のテストは通すことはできるので。
⑧ではmockRepositoryのGetメソッドが1回呼ばれているかどうかをチェックしています。
まとめ
Zenejctとかを使う場合で依存が発生する場合のテストの例を挙げました
- ZenjectUnitTestFixtureを使うとContainerを利用したUnit Testができる
- MockフレームワークとInterfaceを利用することで対象のテストにフォーカスできる
正直UniRxの非同期の部分をこのようにテストすべきかどうかはちょっと不明です。 そのほかPlayModeTestを利用する方法もありますが、オーバーヘッドが大きいような?
それとMoqを利用しましたが、 MockライブラリとしてはNSubstitute(http://nsubstitute.github.io/)のほうが扱いやすい感じ。 ただMoqの方はZenjectにバンドルされており、単純なMockであればもっと簡単にかけたりします。
Zenject BindのAsSingle, AsCached, AsTransientの違い
Zenjectを使い始めて、最初の方に?ってなるのがAsSingle(), AsCached(), AsTransient()な気がします。 正直ZenjectのREADMEを何度読んでもイマイチよくわからない。 いろいろ挙動を調べたので解説します。
Scopeとは
AsSingle(), AsCached(), AsTransient()はZenjectでScopeと呼ばれるものです。 READMEによると「生成されたインスタンスが再利用される頻度」みたいなことが書いてあります。 日本語もしくは英語でOKな説明ですが、Scopeは対象のインスタンスがDIコンテナにて要求されたときに、 新しく生成するのか、同じものを使いまわしてInjectするのかを指定するメソッドになります。
ひとまず、Scopeの指定を何もせずに何かをBindしてみると...
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>(); } }
エラーになります。
ZenjectException: Assert hit! Scope must be set for the previous binding! Please either specify AsTransient, AsCached, or AsSingle. Last binding: Contract: Sample, Identifier: NULL
READMEではDefault: AsTransientみたいなことが書いてありますが、 何かしら指定しないといけないようです。
使い方は以下みたいなかんじです。
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsSingle(); } }
シンプルな場合のAsSingle, AsCached, AsTransientの違い
まず、前置きとして、 同じコンテナに同じタイプのインスタンスは、そのままでは複数個バインドできません。 これはScopeをつけても同じです。
つまり、以下はいずれもエラーになります。
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsSingle(); Container.Bind<Sample>().AsSingle(); // エラー } } public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsTransient(); Container.Bind<Sample>().AsTransient(); // エラー } } public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsCached(); Container.Bind<Sample>().AsCached(); // エラー } }
それを踏まえて三者の違いを比較します。
AsSingle
AsSingleはコンテナ内でインスタンスが1つであることを保証するように動作します。 複数のオブジェクトからInjectの要求があったときに、必ず同じインスタンスが注入されます。
AsCached
AsCachedはもし利用できるインスタンスがコンテナ内に存在すれば、 そのインスタンスをInject時に利用します。 ぶっちゃけ、このシンプルな例ではAsSingleとほぼ等価です。
AsTransient
AsTransientはインスタンスの再利用をまったく行いません。 Injectの要求があるたびに、コンストラクションのフローが走り、 それぞれのクラスに、別々のインスタンスが注入されます。
AsSingleとAsCachedのちがい
正直、AsSingleとAsTransientはわかりやすいです。 AsSingleはSingletonのように1つしか存在しない、AsTransientは毎回別物が生成されるといった覚え方で大丈夫そう。
ではAsCachedとAsSingleのちがいは...?
実はいくつかのケースで違いがあります。 その一つがIdを指定するケースです。 Zenjectでは以下のようにWithIdメソッドでBindするインスタンスにIdをつけ、Inject時にそのIdを指定できます。
using UnityEngine; using Zenject; public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsCached(); Container.Bind<Sample>().WithId(1).AsCached(); Container.Bind<Sample>().WithId(2).AsCached(); } } class ClassA { [Inject(Id = 1)] Sample sample; }
つまり、Idをつけ識別可能な状態であれば、 AsCachedは複数のBindの指定が可能で、別々のインスタンスを生成します。
逆にAsSingleはIdをつけようが何をしようがエラーになります。
ちなみにAsTransientでもIdをつけられますが、 インスタンスの再利用はされないため、あんまり意味がないです。 (WithArgumentsを使う場合は意味あるかも)
Sub Container(Context間で親子関係)がある場合
Scopeはコンテナに対しての指定です。 コンテナが異なれば、例え親のコンテナに同じオブジェクトがすでにバインドされていても再度バインドできます。
この場合、インスタンスが要求された際、 同じコンテナ内のインスタンスが優先して解決されます。
また、Idを指定することで親のコンテナの中のインスタンスを指定することもできます。
つまりAsSingleを指定した場合であったとしても、 コンテナごとに複数の同じクラスのインスタンスが生成されるということになります。 ここがSingletonとは違うところですね。
ちなみに親と子でIdが重複したときの挙動ですが、 Bindは可能で自身のコンテナ内のインスタンスを優先している感じです。
InterfaceをBindする場合
InterfaceをBindする場合、実は以下のような記法もエラーになりません。
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsCached(); Container.Bind<ISample>().To<Sample>().AsCached(); } }
その場合、SampleをInjectした方と、ISampleをInjectした方で、違うインスタンスが解決されます。 (おそらくBindInterfacesAndSelfToを使ったほうが予期した動作になりそう)
ちなみにAsSingleを利用したときの以下はエラーになります。(前のバージョンではできた?)
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsSingle(); Container.Bind<ISample>().To<Sample>().AsSingle(); } }
Unbindした場合
Zenject的にはUnbindはあんまりよくない使い方らしいですが、以下のような挙動になります。
- 同じクラスのAsSingleによるUnbind、再Bindはエラーになる。
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsSingle(); Container.Unbind<Sample>(); Container.Bind<Sample>().AsSingle(); } }
- 同じクラスのAsCachedによるUnbind、再Bindはできる。
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsCached(); Container.Unbind<Sample>(); Container.Bind<Sample>().AsCached(); } }
- AsCachedをAsSingleで再Bindするとエラーになる
public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Sample>().AsCached(); Container.Unbind<Sample>(); Container.Bind<Sample>().AsSingle(); } }
まとめ
ZenejctのScopeについて解説しました。
- AsSingleはコンテナ内でなんだかんだ1つのインスタンスになる
- AsTransientは要求されるたびに毎回インスタンシングフローが走る
- AsCachedはできる限りインスタンスを使いましてWithIdとかを利用すると識別とかができる
- AsSingleでもContainerの親子関係を作って複数インスタンスを使いわけることもできる
ただ、Idを使いまくったり、Unbindを使いまくったたり、Interfaceを変な風に使いまくると 多分わけわかんなくなるのでやめたほうが良さげなきがします。
CryptoKittiesでにゃんこ飼ってみた
仮想通貨とかブロックチェーンとか、 大分前から盛り上がってますよね。
どんなことができるのか、 技術的な特徴みたいなところに興味あってちょっとずつ勉強してる次第です。
その中でもブロックチェーンをゲームっぽいものに応用した CryptoKittiesというサービスを前々から耳にしており、 気になっていたのでやってみました。
ただ、仮想通貨も一切保持したりしてなかったので全部最初からです。
というかモチベーションは「猫飼いたい」の一言につきます。 なので、ビットコインとか仮想通貨で儲けたいとか一切ないです。 最短で猫の所持に向かいます。
CryptoKitties
CryptoKittiesは分散アプリケーションのプラットフォームであるEthereum上にて、仮想の猫を売買したり、育てたりするブロックチェーン技術を応用したゲームです。
CryptoKitties | Collect and breed digital cats!
各猫には遺伝子のようなものが設定されており、 それによって見た目が一匹一匹異なります。
猫の絵柄自体は画面のキャプチャとかとればコピーできますが、 その猫の所有権はブロックチェーンの分散台帳技術により明確に決まっているのです。
つまり、猫を単純に愛でる以外にも、 超レアなこの猫は俺の猫!っていう所有欲的な何かを満たして楽しむこともできます。
そして猫の売買もできるので、 過去には1300万円みないな超絶金額で取引された事例もあるそうです。
おおまかな流れ
- ウォレット(MetaMask)の登録
- CryptoKittiesに登録
- 仮想通貨取引の口座開設
- 仮想通貨、Ethereum (ETH)の購入
- ウォレットに送金
- 猫の選定・購入
1. MetaMask
まずウォレットとしてMetaMaskを導入します。 ウォレットは仮想通貨を保管するためのアプリケーションです。
いろいろなタイプやアプリケーションがあるようですが、 CryptoKittiesはMetaMaskというアプリが必要です。
なんかキツネ?でかわいい。
導入方法は検索するとたくさん出てきますが、 以下を参考にさせていただきました。
イーサリアムの便利ウォレットMetamask(メタマスク)の使い方について | ARUTOKO(あるとこ)
2. CryptoKittiesに登録
とりあえず、公式サイトから登録をします。
このとき、ページ下部に出るCookieの許可と、 Chrome ExtensionのMetaMaskにログインしておかないと、うまく進めません。
最近、日本語に対応した模様。
準備ができたら「始めるにゃ!」をクリックします。
登録フローは一般的なWebサイトと同様な感じです。
3. GMOコイン口座開設
CryptoKittiesはEthereumプラットフォーム上のアプリケーションであるため、通貨として仮想通貨Ethereumが必要です。
自分はこの時点で、 仮想通貨を持っていなかったので取引所の口座を開設し取得を行っています。
仮想通貨取引所は多数ありますが、自分はWebで適当に検索し、適当にGMOコインを選択しました。
以下、大まかなフローです。
- アカウント登録
- 免許証・通知カードアップロード
- 審査待ち
- 郵送された口座開設コード入力
アカウント作成後、審査に4日ほど、 そして、郵送送られてくる開設コードを入力する必要があります。
4. 仮想通貨、Ethereum (ETH)の購入
口座の開設が完了したら、Ethereumの取得を行います。
まずは取引所の口座に、日本円を入金します。 いくつかのネット銀行であればWebのフローから即時入金が可能です。
つぎに実際にETHを購入します。
ひとまず、0.03ETHほど買ってみます。
購入時のレートでは大体1600円くらいでした。
5. ウォレットに送金
取引所の口座にて、 ETHの取得ができた後はMetaMaskに送金します。
まずMetaMaskのAdressをコピーします。
そのアドレスを取引所の口座の送金先に指定し送付を実行。
しばらくするとMetaMaskのウォレットに反映されます。
さらに取引履歴からトランザクションがちゃんと処理されていることも確認でき、ブロックチェーンしてる感が味わえます。
6. 猫購入
さて、これで事前準備完了です。
再度CryptoKittiesのサイトに行き、猫を品定めします。 カタログメニューではなく検索メニューからセール用にチェックを入れて探すと、 安めのにゃんこが手に入りそうです。
購入するにゃんこを選んだら購入ボタンから購入を進めます。 その際、MetaMaskの確認画面が開き、取引ETH額と手数料が表示されます。この手数料がマイニング等で得る通貨になるのですね。中央サーバーの代わりに処理をしくれている端末に思いを馳せて、Submitを押します。
そして初めて、購入したにゃんこがこちら。
名前をカフェオレとつけました。 な、なんだ、かわいいじゃねーか、、、
まとめ
今回は、単純に猫が飼いたかったため、CryptoKittiesでにゃんこを購入してみました。
購入しただけで、他のフィーチャーは試せていませんが、 にゃんこ同士を交配させて、見た目を引き継ぐ子孫をつくったりもできます。
他にもCryptoKittiesのにゃんこを使ったサードパーティ制のアプリや、それを支援するプラットフォームKittyVerseなるものが発表されているらしいです。
にゃんこへの投機的な側面は薄れつつあるのかもしれませんが、どんな展開になるのか楽しみです。
また、仮想通貨を初めて触ったこともあり、いろいろ体験できてよかったです。理論や内部実装の勉強もしていますが、実際にサービスに登録し、リアルタイムに通貨送金のトランザクションを発行することで、システムが動作することの感動を感じられました。
参考
ZenjectのGameObjectContextの使い方
ZenjectにおいてSceneContextはとっつきやすいですが、 長い間GameObjectContextってどう使っていいかイマイチだったりしてました。 日本語の解説も少ないですし。
今回はそんなGameObjectContextの使い方を調べたので紹介します。
GameObjectContextの基本とSceneContextとの親子関係、サブコンテナ
GameObjectContextはシーンに配置しておくとで、 そのシーンのSceneContextを親とする、サブのContextとコンテナを作ることができます。
以下のようなSceneInstaller.csとGameObjectInstaller.cs、 ログ出力をするだけのGreeterクラスと、そのクラスを利用するGameObjectSampleクラスを実装してみます。
using UnityEngine; using Zenject; public class Greeter { public void Greet() { Debug.Log("Hello"); } } public class SceneInstaller : MonoInstaller<SceneInstaller> { public override void InstallBindings() { Container.Bind<Greeter>().AsSingle(); } }
using UnityEngine; using Zenject; public class GameObjectSample : IInitializable { [Inject] Greeter greeter; public void Initialize() { greeter.Greet(); } } public class GameObjectInstaller : MonoInstaller<GameObjectInstaller> { public override void InstallBindings() { Container.BindInterfacesTo<GameObjectSample>().AsSingle(); } }
ちなみにGameObjectSampleにはIInitializableインターフェースを付与し、 Installer側でBindInterfacesToでバインドしています。 これによりContextのResolve後にInitialize関数がコールされることになります。
実装したら、シーン上のContextを設定します。 Scene ContextにはSceneInstaller.csをアタッチし、Installersプロパティに追加します(こちらは説明割愛)。
GameObjectContextはScene Contextと同様ですが、 以下のようにCreate Menu -> Zenject -> Game Object Contextから生成できるため、 シーン上に配置し同じようにGameObjectInstaller.csをアタッチ、こちらもInstallerプロパティに追加します。
これで設定が完了です。 シーンを再生すると、Context間の親子関係が設定され SceneContextにバインドされたGreeterクラスのインスタンスが、 GameObjectContext内にあるGameObjectSampleクラス内にInjectされ、 Initialize関数内のgreeter.Greet()を無事コールすることができました。
これが、GameObjectContextの基本です。 今回はGameObjectInstaller内では1つのクラスしかBindしていませんが、 SceneContextと同様に複数の要素をバインドしInjectし合うことができます。
また、親子関係のルールはSceneContextのParentingと同じで、 子供Contextは親のコンテナの中の要素を使ってBindできますが、 親が子供のコンテナの中の要素をつかってBindはできません。
このサブコンテナという概念はそれなりに有用です。 一つの大きなSceneContextにすべてをバインドすると、 どこからでもなんでもBindできる結果になり疎結合なのかどうか不明になります。 サブコンテナを使い、局所、局所で必要なものだけをBindできる状態にしておくことでカプセル化を達成することができます。
なお、サブコンテナの作り方にはGameObjectContextを使わない方法(FromSubContainerResolve)もありますが今回は割愛します。
ZenjectBindingコンポーネントによるMonoBehaviourのBind
GameObjectContextでもZenjectBindingコンポーネントを使って MonoBehaviourをシーン上の設定だけでInstallerを使わずにContextにバインドできます。
たとえば、なんでもいいのでMonoBehaviourのSubObjectというコンポーネントを作り、GameObjectSampleクラス内で利用したい場合を考えます。 つまり、以下のようにGameObjectSample内に[Inject] SubObjectと記述する感じになります。
using UnityEngine; using Zenject; public class GameObjectSample : IInitializable { [Inject] Greeter greeter; [Inject] SubObject subject; public void Initialize() { greeter.Greet(); Debug.Log(subject.name); } } public class GameObjectInstaller : MonoInstaller<GameObjectInstaller> { public override void InstallBindings() { Container.BindInterfacesTo<GameObjectSample>().AsSingle(); } }
このとき、GameObjectContextをアタッチしてあるGameObjectもしくは、子供のGameObjectにZenjectBindingを配置します。(例として子供のGameObjectを作っています)
このように、設定を行うとGameObjectContext内でSubObjectインスタンスがきちんと解決されます。(SubObjectインスタンスはGameObjectContext内にBindされるので、SceneContextから見えません)
なお、GameObjectContextで親子関係を作ることで階層化させることもでき、 このときZenjectBindingのBinding先のContextは階層で一番近い親のContextになります。
もし、一番近いContext以外にしたい場合はZenjectBindingのContextのプロパティに明示的にContextを指定しておきます。
GameObjectContextの動的生成、そしてFacade
上記の例はGameObjectContextをシーンに事前配置していましたが、 動的に生成することもできます。ただし普通にInstantiateしても動きません。
まずは、第一歩として「シーンの初期化時」に動的にGameObjectContextを生成するようにしてみます。
準備としてGameObjectContextをプレファブ化し、シーンから削除します。
そして、SceneInstallerを以下のように修正します。
using UnityEngine; using Zenject; public class Greeter { public void Greet() { Debug.Log("Hello"); } } public class SceneInstaller : MonoInstaller<SceneInstaller> { [SerializeField] GameObject gameObjectContextPrefab; public override void InstallBindings() { Container.Bind<Greeter>().AsSingle(); Container.Bind<GameObjectSample>() .FromSubContainerResolve() .ByNewContextPrefab(gameObjectContextPrefab) .AsSingle() .NonLazy(); } }
GameObjectSampleをSceneContextにもBindする流れなのですが、 かなり大仰な感じになりますね...
ここでポイントはFromSubContainerResolveです。 これは、 他のコンテナを現在のContextのコンテナのサブコンテナとして追加し、 Bindで指定した要素をサブコンテナの要素から1つだけ選び、現在のContextにBindするという記法です。
さらに、ByNewContextPrefabでGameObjectContextのプレファブを指定し、NonLazy()つけて起動時に生成させるようにしています。
これにより、FromSubContainerResolve内部にて、ByNewContextPrefabで指定したGameObjectContextのコンテナをSceneContextのサブコンテナとして追加し、その中からGameObjectSampleを探しだしSceneContextにBindします。
また、現状態だと、GameObjectContextにはGameObjectSampleのInterfaceしかBindしていないため、自身もBindするようにGameObjectInstallerを変更します。
using UnityEngine; using Zenject; public class GameObjectSample : IInitializable { [Inject] Greeter greeter; [Inject] SubObject subject; public void Initialize() { greeter.Greet(); Debug.Log(subject.name); } } public class GameObjectInstaller : MonoInstaller<GameObjectInstaller> { public override void InstallBindings() { Container.BindInterfacesAndSelfTo<GameObjectSample>().AsSingle(); } }
バインドの指定をBindInterfacesAndSelfToに変更しました。 これでGameObjectContext内でGameObjectSampleとIInitializableの両方をResolveすることが可能になりました。
ちょっとここまで意味不明だったかもしれませんが、 SceneContextのgameObjectContextプロパティに先程プレファブ化したGameObjectContextプレファブを指定し、シーンを再生してみます。
シーン開始時にちゃんとGameObjectContextが生成され、 BindやInjectが成功しログが出力されることが確認できます。
さて、ここで現状でGameObjectSampleクラスは GameObjectContextにバインドされているわけですが、 そのContextのFacadeとして、つまりGameObjectContextにBindしたクラスの窓口や代表者として動作していることになります。
今回の例では、GameObjectSampleインスタンスはSceneContextにもBindされることで、SceneContext内の他の要素とお互いにやりとりすることができます。しかし、SceneContext内の他の要素はGameObjectContext内の他の要素をInjectしたり等、直接アクセスすることはできません。つまりGameObjectContextの世界とやり取りしたい場合は、その代表者と定めたGameObjectSampleクラスを介在する必要があります。
FromSubContainerResolveを利用することにより、SceneContextから見るとBindするものは、GameObjectContextなのか単体のコンポーネントなのかは気にせずに柔軟が使い方ができます。
たとえば、Bindしているクラスが予想以上に肥大化してしまった場合に、 GameObjectContextを使ことでDIによる可用性を保ちながら、サブコンポーネントに分割することや、GameObjectContextとして運用しているものを単純なコンポーネントに置き換える場合等でも大きな変更がいりません。
FactoryによるGameObjectContextの動的生成
さらにFactoryを利用することでGameObjectContextを任意タイミングで生成することができます。
以下のようにGameObjectSampleのFactoryをBindする形式に変更し、 GameObjectContextCreaterというFactoryをクリックで利用する適当なクラスを作ります。
using UnityEngine; using Zenject; public class Greeter { public void Greet() { Debug.Log("Hello"); } } public class GameObjectContextCreater : ITickable { [Inject] PlaceholderFactory<GameObjectSample> factory; public void Tick() { if(Input.GetButtonDown("Fire1")) factory.Create(); } } public class SceneInstaller : MonoInstaller<SceneInstaller> { [SerializeField] GameObject gameObjectContextPrefab; public override void InstallBindings() { Container.Bind<Greeter>().AsSingle(); Container.BindFactory<GameObjectSample, PlaceholderFactory<GameObjectSample>>() .FromSubContainerResolve() .ByNewContextPrefab(gameObjectContextPrefab); Container.BindInterfacesTo<GameObjectContextCreater>().AsSingle(); } }
GameObjectSampleのFactory利用は直接クラスをBind指定するのではなくContainer.BindFactoryでFactoryのBindを行えばOKです。(最近のZenjectではPlaceholderFactoryとして汎用的なFactoryを利用できます)
このときGameObjectContextCreaterはITickableをつけることでm毎フレームTick関数をZenject側からコールしてもらい、Inputクラスにより入力を検知するようにしています。(サンプルな懐かしい書き方)
これで、クッリクするたびにGameObjectContextが生成されます。
まとめ
GameObjectContextの使い方を紹介しました。
- GameObjectContextはSceneContextの子となりサブコンテナを形成できる
- ZenjectBindingは一番近いContextにBindしにいく
- FromSubContainerResolveを使うと親のContextにFacadeとしてGameObjextContextのコンポーネントをBindできる
- Factoryを使うことでGameObjectContextを動的に生成することができる
GameObjectContextはSceneContextと違い、より手軽にContextを生み出すことや、破棄することができます。また親子関係の管理も楽です(Sceneが破棄されると同時に破棄される)。取り回しがしやすいのでぜひ使って行きたいです。おそらくこの後はどれをSceneContextとし、どれをGameObjectContextにするのかという議論も必要そうですが。
ZenjectDefaultSceneContractConfigで依存するSceneContextを確認用に自動ロード
UnityのZenjectの他の機能の紹介をしようかと思ったところ、 ZenjectDefaultSceneContractConfigという機能を発見。
おそらくv6.1.0くらいから追加されたっぽいんですが、 個人的には全俺が湧く感じの機能、圧倒的僥倖だったのでこちらを紹介します。
Project Contextの使いにくさ
まず、Zenjectを使う上で、ProjectContextというものがあります。 これは、何かしらのScene Contextが読み込まれる前に、 必ず自動的にロードされるContextで、 アプリケーション起動中に常に必要なものをBindしておくには非常に便利なものです。
ただし、このProjectContextには「どんなシチュエーションでも有無を言わずロードされるため、コンテキストの差し替えが効かない」 という積極的に使用できない欠点がありました。 例えば、タイトル画面がら起動する通常フローはサーバーへ通信を行うコンポーネント群を利用するが、 Editor上での特定テストフローの場合は、 ローカルのフィクスチャファイルからデータをロードするモックコンポーネント を利用する場合等が挙げられます。 これらのような、場合によってはInstallerを差し替えたいというケースの場合は ProjectContextではやりにくかったのです。(内部で処理を分岐させれば可能)
ということで、せっかく自動で前提条件をロードしてくれるContextがあるんですが、 切り替えが生じるContextはSceneContextで実装し、 Scene ContextのAuto Runを切った上で、 自前で依存するSceneContextをロードするフローなどを作ったりしてました。
ZenjectDefaultSceneContractConfigによる自動Contextロード
たとえば、以下のようなSampleシーンのContextが DefaultシーンのDefaultというContract NameのContextに依存しているとします。 こういったシチュエーションの場合、 このシーンの起動にはDefaultシーンを予めロードしておく必要があります。
ゲーム起動の通常フローならば、 おそらくDefaultシーンを誰かがロードするようになっていると思いますが(Build Settings / LoadLevel 0でも)、 EditorでSampleシーンを編集後、動作を確認したい場合、Playボタンを押して、このシーンから直接始めたくなります。 つまり、依存するContextは自動でロードしてほしい。
これが、 ZenjectDefaultSceneContractConfigという機能を使うとできます。
使い方はシンプルで、 まずは、Resources以下にZenjectメニューからZenjectDefaultSceneContractConfigアセット作ります。
そして、デフォルトロード対象のContractNameとシーンファイルを設定します。
これで完了です。 Unity上でPlayボタンを押すと、自動的にDefaultシーンがロードされ再生が始まります。 (停止時に自動ロードされたシーンはアンロードされません)
また、このZenjectDefaultSceneContractConfigには、複数のDeafult Contextが登録でき、 依存関係の解決を再帰的にできるようです。 例えば、あるScene ContextにはAというContextを持つシーンが必要で、 A ContextはBというContextを持つシーンが必要な場合 A, B両方ともZenjectDefaultSceneContractConfigに設定しておけば、 双方自動でロードをしてくれます。
テスト用のContextとのスイッチ?
さらに、ZenjectDefaultSceneContractConfigで設定したContextは、 Scene ContextにおいてParent Contract Nameが設定されている状態かつ、 まだシーンにロードされていないときのみ、ロードの処理が走ります。
つまり、予めDefaultシーンのContextと同じContract Nameを設定した別のContext、 例えばテスト用のDefaultシーンを用意しPlay前にロードしておけば、 コードを一切変える必要なく、動作の切り替えができそうです。
ちなみにこのZenjectDefaultSceneContractConfigはEditorのみの機能のようで、 通常のビルドでは明示的に依存シーンをロードするフローが必要です。 そのため、今回のように、あるシーンが他のシーンに依存するが、 確認フローやテスト等で直接起動したい場合のワークフローに有効そうです。
むしろEditorのみの機能だとしたら、 DefaultSceneContractConfigの方をモックやスタブしたContextにしておいたほうが良いのかもしれませんが。