CQRS+ESについて細かい実装や考察をまとめてみた
前提
私は「エリック・エヴァンスのドメイン駆動設計」を読んだのみで、「実践ドメイン入門」は未読(とても欲しい)の状態で書いています。 (「実践ドメイン入門」にはもっと深い洞察が書いてあるのだと思います…) また「エリック・エヴァンスのドメイン駆動設計」と共に、参考リンク先をまず読んだことがある前提で書いている部分がありますのでご注意下さい。今後もっと良いアイデアが浮かんだり、実際に実装するにあたって浮かび上がった課題があれば随時追記・記事にできればいいなと思います。
参考
CQRS+ES
DDDから更にData Flowの概念を取り込んだarchitecture。 コマンドクエリ分離原則(CQS/Command Query Separation)に基づく。 CQRSは Command Query Responsibility Segregation の略。 ESはEvent Sourcingの略。
それぞれCommand側・Query側についてまとめていく。
Command (Write)側
Command側のフロー
まずCommand側のフローとしては以下のようになっている
- ユーザーは任意のアクションを実行する
- クライアントがアクションをCommandとしてサーバーへ投げる。(例:名前の変更/ChangeName)
- サーバーがCommandを受け取る
- ドメインモデルがCommandを解釈し、ドメインに基づいた処理を実行する(例:名前を変更する)
- ドメインモデルは結果となるEventを生成する(例:名前が変更された/ChangedName)
- Eventを保存する
Command受付からドメインモデルでの処理へ
Command と Event
Commandは指示、Eventは指示の結果として起きた出来事。意図がまったく異なるので混同しないように注意が必要。クラスとしても分けるべき。命名にもルールがあって、Commandは常に命令形で定義(例: Change Name)し、Eventは常に過去形で定義する(例: Changed Name)
タスクベースのUIで考える
コマンドとして扱うにはタスクベースのUIにするのが良い。
よく見かけるのはCRUDや管理画面等で表の状態を表示し、「保存」ボタンがあり、各formとデータが関連して定義されたDTO(DataTransferObject)が存在し、「保存」アクションで関連付けられたDTOをそのまま送信する、などのパターンだが、このようなものとは異なる。
例えば名前を変更するときは、名前を「保存/更新する」のではなく「名前の変更」というタスクとして切り分け、DTOをそのまま送信しない。「名前の変更」コマンドの中身は、変更者のユーザーID、コマンド名、変更後の名前のみとなる。
UIのためのデータ生成はコストが大きい
ドメインオブジェクトをDTOに変換するのはDataMapperを必要とするうえ、カプセル化して守られているはずの中身をさらけ出す行為に等しい。すべてのモデルから、dumpするためのメソッドを用意するか、このためだけにgetterを生やすことになってしまう。もっとキレイに書くこともできるかもしれないが、いずれにしてもカプセル化し閉じておくべきデータを外に出す入り口を作ってしまう。
[DomainObject] => data mapping => [Data Transfer Object] => client
CommandとQueryを分け、Query側には薄いDataLayerから直接DTOを生成すれば、ドメインオブジェクトはCommand側でのみ使われることになり、カプセル化を破ってまでDTOを生成する必要はなくなる。
EventStoreへの保存からSnapShot作成へ
CommandをEventに変換してEventStoreに保存してからの振る舞い、実際の実装方法においては詰めたい点が残る。CQRSについて述べているものを見ても、以下のように分かれている。
- EventStoreを保存して、そのEventStoreから読み出してSnapshotを作る(By Greg)
- EventStoreに保存すると同時にEventHandlerにEventを送る(By Nijhof)
両者とも理屈はわかる。まずGreg側はトランザクションに守られないならばflowは分岐せず1つであるべきという意見。この実装においてはどのEventをHandlingしたか、が分かるようにしなければならない。Gregによると通し番号(sequence)を付けておくのがよい、と書かれている。
対して、NijhofはRead側をRDBMSを想定しているのでインピーダンスミスマッチは起こるが吸収できれば問題なさそう。
EventStore Schema
Event Table, Aggregate Table, SnapShot Table が必要になりそう。
Event Table
発生したイベントを保存するテーブル。 INSERTで追記するのみで、UPDATE/DELETEはしない。 イベントはこの後でEventHandlerに渡すので、ドメイン処理した環境とは 別の環境でも読み取れるよう、JSONなどの汎用フォーマットにしておくとよさそう。
Name | Data type | Content |
---|---|---|
Sequence Number | Big Int | イベント通し番号 (Auto Increment) [PK] |
Aggregate ID | String | 集約ルート ID (UUID) |
Data | Blob | シリアライズされたEventデータ |
Version | Int | 集約ルートデータバージョン |
Created At | Timestamp | イベント発生日時 |
Aggregate Table
イベント対象となる集約ルートの最新バージョンを保持する。
Name | Data type | Content |
---|---|---|
Aggregate ID | String | 集約ルート ID (UUID) [PK] |
Type | VarChar | 集約ルート名 |
Version | Int | 集約ルートデータバージョン |
Updated At | Timestamp | 更新日時 |
Snapshot Table
イベントをversionの順に適用していった結果の集約ルート状態を保存しておくテーブル。 ドメイン処理した集約ルート状態を再現したインスタンスをシリアライズしたものを入れておく。
Name | Data type | Content |
---|---|---|
Aggregate ID | String | 集約ルート ID (UUID) [PK] |
Sequence Number | Big Int | イベント通し番号 (Auto Increment) |
Serialized Data | Blob | シリアライズされた集約ルートのスナップショットデータ |
Version | Int | 集約ルートデータバージョン |
Updated At | Timestamp | 更新日時 |
格納手順
まずAggregateテーブルからAggregateIDでSELECTする
SELECT version FROM aggregates WHERE aggregate_id = [aggregateId]
空の場合はaggregate_idとversionを0
にして結果セットとする
得たaggregate結果セットrowのversionをwhere句に追加し、一致したときにINSERTするようにする。 後続のeventのrowも続けて入れていく。
aggregatesの最新バージョンを得ておく
SELECT version FROM aggregates WHERE aggregate_id = [aggregate_id] FOR UPDATE
トランザクションを開始する
BEGIN
イベントを追加していくが、追加されるイベント数分のversionを上げて更新する
UPDATE aggregates SET version = version + [after_version] WHERE aggregate_id = [aggregate_id] AND version = [expected_before_version]
ただしく成功されたか(影響行数が1になっているか)確認して続ける。 FOR UPDATE 文なので大丈夫なはずだが、万一失敗したらここでrollbackする
個々のeventをinsertしていく
INSERT INTO events(aggregate_id, data, version) VALUES([aggregate_id], [serialized_event_data_1], [version_1]); INSERT INTO events(aggregate_id, data, version) VALUES([aggregate_id], [serialized_event_data_2], [version_2]); INSERT INTO events(aggregate_id, data, version) VALUES([aggregate_id], [serialized_event_data_3], [version_3]); INSERT INTO events(aggregate_id, data, version) VALUES([aggregate_id], [serialized_event_data_4], [version_4]);
最後にcommitする
COMMIT
ここまでで Commandとしてのフローは完了するので、Clientにレスポンスを返していく。
SnapShot生成
backgroundのプロセスがeventテーブルを監視しておき、前回処理した最終のsequence_numberからすべてを取得していく
SELECT sequence_number, aggregate_id, data, version FROM events WHERE sequence_number > [last_processed_sequence_number] ORDER BY sequence_number ASC;
これを順次処理していく。取り出したaggregate_idから前回までのsnapshotを取り出す
BEGIN
SELECT aggregate_id, sequence_number, serialized_data, version FROM snapshots WHERE aggregate_id = [aggregate_id] FOR UPDATE
取り出したデータをunserializeして集約ルートを復元し、Eventを順次適用していく 最後のEventまで適用が終わったらupdateしていく
UPDATE snapshots SET sequence_number = [sequence_number], serialized_data = [serialized_data], version = [version] WHERE aggregate_id = [aggregate_id]
完了したらcommitする
COMMIT
SnapShotからの復元
まず最初は同様にAggregateテーブルからAggregateIDでSELECTする
SELECT version FROM aggregates WHERE aggregate_id = [aggregateId]
snapshotをAggregateIDから取得
SELECT aggregate_id, sequence_number, serialized_data, version FROM snapshots WHERE aggregate_id = [aggregate_id]
eventsからsnapshotからの差分を取得する。 得たeventを適用(replay)していき、集約ルートを構築していく
SELECT sequence_number, data, version FROM events WHERE aggregate_id = [aggregate_id] ORDER BY sequence_number ASC
データの流れで問題になりそうなところ
Command(Write)側では複雑なQueryは使えない
Gregも言っているとおりEventSourcingの場合、Command(Write)側で扱えるQueryはPrimaryKeyに基づくものだけになる。 このパターンはユーザーIDで紐づくのはUserの集約になるので、ほぼユーザーデータをまるごとになり、大きめのデータを扱うことになる。 Command側ではこの制約下でも問題ないように作る必要がある。
Eventを記録するときは状態を変化させない
これが一番理解に時間がかかった点ですが、納得してみれば確かにな、と思った箇所。
集約ルートがEventを受け取ったらEventStoreに登録(Apply)するだけにとどめ、集約ルート自身の状態を変化させない。 これまでの過去の結果(snapshot+events)である集合ルートに対して、Eventを生成し、EventStoreに登録するのみ。
これはつまり、ある集合ルートはnameプロパティを持ち、fooという文字列が設定されているとした場合、この集合ルートのnameをbarに変更するコマンドChangeNameCommand
を渡したとすると、
nameをfooからbarに変更するイベントChangedName
をEventStoreに登録するが、実際にnameプロパティは変更せず、fooのままにしておくということです。
これは、集合ルートに適用したとしてもその結果を返すことはしないので、状態を変化させることに意味はないからだと考えます。 Snapshotを作る時には過去の結果(snapshot+events)に今回のEventを適用した状態をSnapshotにするので問題にはならないし、Snapshotを作る前であっても過去の結果(snapshot+events)の中に入ってくるのでこれも問題にはならない。
Push型か、Pull型か
クライアントからのCommandを実行し、Eventを登録するまでは同じですが、EventHandlerをkickする(push型)かpolling(pull型)かで処理の流れが異なる。
タイプ | 特長 |
---|---|
Push型 | Commandの処理を行う際に、EventHandlerにEventを流す |
Pull型 | Commandの処理を行う際に、Eventの登録まで行いPollingして検出する |
EventSchema(Write)側のデータから、ReadModel(Read)側へデータが流れるとき Read側がEventSourcingでなく、RDBMSで状態を表す伝統的な仕組みの場合は一度実行されたイベントが再度実行されてはならない。
Push型
EventHandlerにPushするならば、同期的にすべて行うとパフォーマンスに影響が出るので非同期的に行うほうが良い。ただ処理結果は待たずに返さないと結局処理を待つことになるので、指示だけを送って早々にレスポンスを返す事になりそう。
Pull型
Pull型のもっともシンプルな流れでは、下記の3段階でReadModelに適用が行われると思われる
- EventのQueueからの取り出し
- QueueからReadModelへの適用
- EventのQueueからの削除
このとき、2までは成功したが3に失敗したとき、2をrollbackできなくてはいけない。 QueueとReadModelにまたがってトランザクション処理が行えれば良いが、将来的なスケーラビリティを確保するためにも分離されていたほうが良いと思われる。
それぞれトランザクションを用意する
時系列でこんなかんじ
| => begin o => commit d => dequeue p => published @ => insert/update Queue ---|----d--------------p------o--------------------> RDBMS -----------|-----@----------------o---------------->
便宜上RDBMSのcommitはqueueを先にRDBMSを後にしたが、どちらが先でも構わない。 大事なのは、QueueもRDBMSも処理を終え、成功を確認したらcommitするということ。
EventHandlerもそれなりに大変
Eventを生成することもDDDドメインモデルの振る舞いが大切になるが、 EventをHandlingしてReadModelに落とす場面も注意深く実装しなくてはいけない。
Read側もEventSourceを格納するのか、正規化してRDBMSに落とし込むのか
からなずしもEventSourcing形式でなくても良いが、RDBMS側はしっかり考えて設計を行う必要がある。なんらかのCommandで、関連する集合を引く必要がある時に、EventStoreデータだけでは引けないことがある。この場合、ReadModelから引くがAggregateIDがないと集合ルートを発見できないので、RDBMSにもAggregateIDを関連させて保存しておく必要がある。
RDBMSに正規化して保存するなら、インピーダンスミスマッチを考慮しつつ、「読み込みやすいこと」「パフォーマンスが出ること」を優先して保存していく。場合によっては非正規化したViewを用意することも考慮に入れていく。
最初は1プロセスでRDBMS保存まで行ってもいい
最初からMicroservice化してしまうとコストが回収できない場合があるので、最初からやりすぎないことが大事。
Query (Read)側
ここまでで非常によく練った状態でRDBMSに格納していったので、Query側は取り出すだけでよい。というかそのようにする。パフォーマンス、スケーラビリティを考慮してなるべくシンプルに保つと良さそう。
まとめ
DDDの弱点であるコストが大きくかかる点であったり、DTOへの変換がOOP原則に照らして辛い部分などをクリアする、とてもよく考えられた設計だなと感じました。これまでのいわゆるMVCなどのアーキテクチャに慣れていた人(自分)には結構なマインド変換が必要なものだなと。 まだ実際には稼働させていないのですべて机上の考察なので、もっと深い考察が出て来るでしょうが、楽しくなりそうです。
Docker for mac で nginx を起動するサンプルが動かないとき
Docker for mac 入れる
こちらを参考にして入れてみる。
Version
% docker --version Docker version 1.13.1, build 092cba3 % docker-compose --version docker-compose version 1.11.1, build 7c5d5e4 % docker-machine --version docker-machine version 0.9.0, build 15fd4c7
Step 3 でエラーが起きる
nginxイメージを起動できない。
% docker run -d -p 80:80 --name webserver nginx Unable to find image 'nginx:latest' locally docker: Error response from daemon: Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io on 192.168.65.1:53: server misbehaving. See 'docker run --help'.
でgithubのissueにもある問題っぽい
で、上記の参考にさせてもらったブログを見ると、betaにすると治ったらしいのでbetaにする (stableをuninstallしてbetaをDL、インストール)
% docker --version Docker version 1.13.1, build 092cba3 % docker-compose --version docker-compose version 1.11.1, build 7c5d5e4 % docker-machine --version docker-machine version 0.9.0, build 15fd4c7
変わらないようにみえる…
気を取り直して再実行
% docker run -d -p 80:80 --name webserver nginx Unable to find image 'nginx:latest' locally latest: Pulling from library/nginx 5040bd298390: Pull complete 333547110842: Pull complete 4df1e44d2a7a: Pull complete Digest: sha256:f2d384a6ca8ada733df555be3edc427f2e5f285ebf468aae940843de8cf74645 Status: Downloaded newer image for nginx:latest 62ed7ad57e72753812205e627cb9b35ae0ee66a54a51bd4f895cf1ede9337628
入った。
が、 http://localhost:80/ を叩いても接続できない。
docker ps
を叩いても、ちゃんと大丈夫そうに見える。
% docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 89ec878940a4 nginx "nginx -g 'daemon ..." 12 minutes ago Up 12 minutes 0.0.0.0:80->80/tcp, 443/tcp webserver
Mac本体のportを見てもListenしてるように見える。
% sudo lsof -i -P | grep LISTEN | grep 80rpcbind 280 daemon 4u IPv4 0x7fa9588305b9f605 0t0 TCP *:111 (LISTEN) // 略 com.docke 35368 edy 25u IPv4 0x7fa958831ccb1605 0t0 TCP *:80 (LISTEN) // 略
stackoverflowに聞いてみると同様の人がいる。
http://127.0.0.1 だと動くかもっていう話だったから試したら動いた。マジで…。 そもそもnginxの設定にlocalhostで受けるようになってないだけ?
2017/02/18追記
ホストMacのhostsにlocalhostをIPv6で解決する記述があったため、これをコメントアウトしたら動いた。
% cat /etc/hosts # 略 #::1 localhost
Swift用依存管理マネージャのCarthageを導入する
Swift用依存管理マネージャのCarthageを導入する
前回のCocoaPodsは結局採用せずCarthageを採用した理由は以下の通り。
- 元々のxcodeprojを使えないので、何かあった時に情報が少なそう(Carthageは元のxcodeprojを使うのでシンプル)
- xcworkspaceについての情報が少ない(Carthageはxcodeprojのまま)
- AlamofireがCarthageに対応している
Carthageとは?
CocoaPodsと同様に、Cocoaライブラリ依存管理ツールです。下記の2記事が最も詳しく解説されています。
(ちなみにCarthageとググるとカルタゴが出てきますが、チュニジア共和国の古代都市国家だそうで。何故この名前になったのだろう。)
Carthageの仕組み
上記にも言及されていますが、Carthageはdynamic frameworkを作成するとのこと。
一方、CarthageはリポジトリのXcodeプロジェクトの情報をそのまま利用してdynamic frameworkを作成します。
dynamic framework は iOS8以上 対応なのでそれ以下は導入できないらしい。 これ以下のiOSに対応する事はできないので注意が必要。
dynamic frameworkってつまり何?と思ってググると、dynamic library とかstatic library とかがごちゃ混ぜになりつつ、わかってる体で話が進んでいる記事が多いので、 dynamic frameworkとはなんぞや、から探してみると、stackoverflowがヒット。
ios - Library? Static? Dynamic? Or Framework? Project inside another project - Stack Overflow
libraryはコードのみ、frameworkはassetsを含められる。staticはコンパイル時、dynamicはランタイム時ということらしい。static/dynamicはごく普通なので問題なかったですが、framework/libraryの違いがわかってよかった。
Carthageのインストール
homebrewでインストールできる。
$ brew update $ brew install carthage
以上。
Cartfileを作成
BundlerでいうGemfileみたいな感じで依存性をCartfile
というファイルに書いていく。
OriginとVersion requirementを追加
Originはgithubリポジトリとして書くか、gitのrepositoryパスを指定する。 Version requirementはバージョン番号のほか、gitのbranch名やcommitのhashを指定できる。
今回は前回のCocoaPodsの時と同様に、Alamofireを導入する。
$ cat Cartfile github "Alamofire/Alamofire" >= 1.2
カンタン!
carthage updateを実行
Cartfileに追加したライブラリを実際に利用できるようにする。
$ carthage update *** Cloning Alamofire *** Checking out Alamofire at "1.2.2" *** xcodebuild output can be found in /var/folders/2s/zm_3tqmd2kl9dfk44q55g0s00000gn/T/carthage-xcodebuild.uyO8hk.log *** Building scheme "Alamofire iOS" in Alamofire.xcworkspace *** Building scheme "Alamofire OSX" in Alamofire.xcworkspace
carthage update
を叩くとCarthage/Checkouts
ディレクトリが作成され、利用するそれぞれのライブラリ名でcloneされる。
$ ls Carthage/Checkouts/Alamofire Alamofire.playground Alamofire.xcodeproj CHANGELOG.md Carthage LICENSE Source iOS Example.xcodeproj Alamofire.podspec Alamofire.xcworkspace CONTRIBUTING.md Example README.md Tests
また、前述したようにdynamic frameworkとしてbuildされる。 ここまで自動でやってくれるので非常に便利。
$ ls Carthage/Build/iOS/Alamofire.framework Alamofire Headers Info.plist Modules _CodeSignature
Xcodeに設定を追加
ライブラリを使えるよう、Xcodeでも設定を追加します。
- “General” -> “Linked Frameworks and Libraries” から、"+“をクリックし先ほどビルドした
Carthage/Build/iOS/Alamofir.framework
を追加 - “Build Phases” -> “+” -> “New Run Script Phase” から、Scriptとして下記を追加
/usr/local/bin/carthage copy-frameworks
これだけでうごく!と思いきやエラーが。 同じエラーが出ている人もいるようで見てみる。
どうも皆 “General” -> “Embedded Binaries"にも追加しているので、やってみると成功した。 これでswfitから
import Alamofire
とすれば利用できます。
まとめ
- CocoaPodsに比べると導入がちょっと手間がかかるけど、管理とかハマりを考えたら楽だと思う。
- Carthageカンタン便利!対応しているならどんどん使っていくと良いと思う。
- CarthageのついでにReactiveCocoaとか面白そうだけど、しょっぱなからこれはどうだろうという思いと、趣味だしいいんじゃねーの感の葛藤がある
Swift用依存管理マネージャのCocoaPodsを導入する
iOSアプリ開発時にHTTP通信が必要になったのでライブラリを探していると、CocoaPods と、同様のツールでわりと新しめのCarthageというのがあった。
最終的にはCarthageを採用したので別の記事に書きますが、CocoaPodsもインストール試してみたので、導入の備忘録と感想を残しておきます。
Cocoapodsとは?
CocoaPods Guides - Getting Started
CocoaPods manages library dependencies for your Xcode projects.
The dependencies for your projects are specified in a single text file called a Podfile. CocoaPods will resolve dependencies between libraries, fetch the resulting source code, then link it together in an Xcode workspace to build your project.
Ultimately the goal is to improve discoverability of, and engagement in, third party open-source libraries by creating a more centralised ecosystem.
頑張って訳すとこんな感じ?(間違ってたらすいません。)
CocoaPodsはあなたのXcodeプロジェクトのライブラリ依存性を管理します。 あなたのプロジェクトへの依存性はPodfileと呼ばれるひとつのテキストファイルで指定されます。 CocoaPodsはライブラリ間の依存性を解決し、結果のソースコードを取得し、その後あなたのプロジェクトをbuildするためにXcodeのworkspaceにリンクします。
要するに依存性解決ツールのようです。Rubyでいうとbundler、(私のように)PHPerからするとcomposerみたいな感じでしょうか。ライブラリも探せるようなので、雰囲気的にはpackagistも兼ねてる感じ。
まずはCocoaPodsをインストールしてみる
gemでインストールします。
$ sudo gem install cocoapods
sudo
を使わない時は--user-install
フラグをつけるなどが推奨されています。
Podfileに依存ライブラリを書く
Xcodeプロジェクトディレクトリ直下でpod init
コマンドを叩くと、Xcodeプロジェクト設定が読み込まれ、Podfileが自動生成されます。
$ pod init $ ls Podfile Podfile
Xcode設定が読み込まれて自動的に設定されるようです。下記は
- プロジェクト名が「hoge」
- 対応バージョンが6.0以上
の設定です。
$ cat Podfile # Uncomment this line to define a global platform for your project # platform :ios, '6.0' target 'hoge' do end target 'hogeTests' do end
Podfileにライブラリを設定する
HTTP通信ライブラリであるAlamofireを使いたいので、README.mdを参考に設定してみます。
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' use_frameworks! target 'hoge' do pod 'Alamofire', '~> 1.2' end
ライブラリをインストールする
設定できたらpod install
します。
$ pod install
*.xcworkspace
というのが出来上がるので、今後はいつものxcodeprojの代わりに、これを使います。
$ open App.xcworkspace #=> Xcodeが開く
CocoaPodsの感想
CocoaPodsは*.xcworkspace
というファイルを作るようで、はてxcworkspaceとは何ぞやということで調べるも、いまいちコレだという資料も見当たらず良くわからずじまい。
要するにこれを使うとライブラリをリンクした状態でプロジェクトを開けるらしいのだけど、仕組みがわからないとハマったときどうにもならなくなりそう。
iPhone/iPad実機からMacのdockerコンテナ環境にリクエストする
iOSアプリを開発するにあたって、APIを受けるサーバ側の開発環境を用意することにした。 Vagrantでもいいのだけど、Dockerにも触っておきたくて入れてみた時の備忘録的なアレです。 正直ネットワーク周りはあまり深く知らないので、もっとうまくやる方法がきっとあると思います。
まずはMacにdockerをインストール
dockerはLinuxカーネル機能を使っているため直接Macで使えないので、
dockerのdaemonを動かす専用の軽量VMとその管理ツール(boot2docker
)が用意されています。
Macから動かすには、このboot2docker
を通じて動かします。
ざっくりこんなイメージ。
Mac <==> VirtualBox(VM) <==> Docker container
boot2dockerの導入
まずMacにVirtualBoxを入れます。 Downloads – Oracle VM VirtualBox
次にこちらを参考にhomebrewからdockerとboot2dockerをインストールします。 qiita.com
初期化
- VMを作成。初回の1度のみ。
mac$ boot2docker init
VM起動
mac$ boot2docker up
環境変数の確認
VMが起動するたびに、VMの$DOCKER_HOSTが変動したりするので以下のコマンド結果を実行します。
mac$ boot2docker shellinit Writing /Users/edy/.boot2docker/certs/boot2docker-vm/ca.pem Writing /Users/edy/.boot2docker/certs/boot2docker-vm/cert.pem Writing /Users/edy/.boot2docker/certs/boot2docker-vm/key.pem export DOCKER_CERT_PATH=/Users/edy/.boot2docker/certs/boot2docker-vm export DOCKER_TLS_VERIFY=1 export DOCKER_HOST=tcp://192.168.59.103:2376
毎回手動で行うのも面倒なのでshellの*rcなどに書いておきます。
# to connect docker server if [ "`boot2docker status`" = "running" ]; then eval $(boot2docker shellinit 2>/dev/null) fi
状態の確認
mac$ boot2docker status running
ここまででとりあえずboot2docker
の導入は終わり。
dockerを使う
使えるイメージを確認する。最初は空。
mac$ docker images
imageの用意
dockerコンテナの元にする、使いたいimageを落とす (debian:wheezyの例)
$ docker pull debian:wheezy
search
サブコマンドでdocker hubから探せる
(例はdebian)
mac$ docker search debian
起動してみる
debian/wheezyから起動してみる。
-i
はinteractive
-t
はttyの割り当て
--name
はコンテナ名の割り当て
mac$ docker run -i -t --name www debian:wheezy /bin/bash
ポートを紐付けたい場合は-p
。
例はホスト側8080をゲスト側80番にフォワードしてる状態。
mac$ docker run -i -t -p 8080:80 --name www debian:wheezy /bin/bash
起動するとシェルに入る
root@cbc18d9ebc9c:/#
-d
でデタッチする
mac$ docker run -d -p 80:8080 --name www debian:wheezy /bin/bash
シェルに入りたいとき
mac$ docker exec -it www bash
状態確認
コンテナの存在確認はps
サブコマンドを使う。
停止されているコンテナも含めるときは-a
オプションを指定する。
mac$ docker ps -a #=> CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES #=> cbc18d9ebc9c debian:wheezy "/bin/bash" 4 minutes ago Exited (0) 3 minutes ago www
イメージづくり
Dockerfileを作る
できればここで頑張りすぎず、他のオーケストレーションツール(ansibleとかchefとか)に任せたほうが幸せな気がします。 下記では最新版のnginxを取ってくるためdotdebの設定を追加しています。
mac$ mkdir nginx mac$ cd nginx mac$ wget http://www.dotdeb.org/dotdeb.gpg mac$ vi Dockerfile FROM debian:wheezy MAINTAINER edy <xxxxxxxxxxx@gmail.com> RUN echo "deb http://packages.dotdeb.org jessie all" > /etc/apt/sources.list.d/dotdeb.list RUN echo "deb-src http://packages.dotdeb.org jessie all" >> /etc/apt/sources.list.d/dotdeb.list RUN echo "deb http://mirrors.teraren.com/dotdeb/ stable all" > /etc/apt/sources.list.d/jp-mirror.list RUN echo "deb-src http://mirrors.teraren.com/dotdeb/ stable all" >> /etc/apt/sources.list.d/jp-mirror.list RUN echo "deb http://mirrors.teraren.com/dotdeb/ wheezy-php56 all" > /etc/apt/sources.list.d/php56.list RUN echo "deb-src http://mirrors.teraren.com/dotdeb/ wheezy-php56 all" >> /etc/apt/sources.list.d/php56.list ADD dotdeb.gpg /tmp/ WORKDIR /tmp RUN apt-key add dotdeb.gpg && \ apt-get update && \ apt-get upgrade -y && \ apt-get install -y nginx && \ apt-get clean && \ rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* ENTRYPOINT /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
こまかい記法はここを参考にしました。 www.atmarkit.co.jp
イメージをbuildする
mac$ docker build -t siny/debian:1.0 .
出来上がったら、イメージを指定して先ほどと同じようにrunします。
mac$ docker run -d -p 8080:80 --name www siny/debian:1.0
ポートフォワード設定
現状のネットワーク
ここまでの設定と状態を整理します。
LAN上のMacのIPは192.168.11.2
となっている状態で、こんな感じになります。
mac$ ifconfig ...略 vboxnet1: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500 ether XX:XX:XX:XX:XX:XX inet 192.168.59.3 netmask 0xffffff00 broadcast 192.168.59.255 mac$ boot2docker ip 192.168.59.103
iPhone [192.168.11.3] || ルータ [192.168.11.1] || [192.168.11.2] Mac [192.168.59.3] || [192.168.59.103] VirtualBox
まずMacのローカルポートをVMへフォワード
この状態でnginxにアクセスするには、Mac上からは192.168.59.103:8080
でアクセスしなくてはいけません。
なので、まずlocalhostのポートをVMのポートへforwardします。
mac$ VBoxManage controlvm "boot2docker-vm" natpf1 "nginx,tcp,127.0.0.1,8080,,80"
これでlocalhost(Mac)の8080番がboot2docker(VM)の80へforwardされるようになり、
127.0.0.1:8080
またはlocalhost:8080
でアクセスできるようになります。
iPhone [192.168.11.3] || ルータ [192.168.11.1] || [192.168.11.2] Mac [192.168.59.3] localhost:8080 || || [192.168.59.103] 192.168.59.103:8080 VirtualBox
MacのLAN側からもフォワード
そしてようやく本題の実機からのアクセスを行うため、Macのいずれかのportを更にフォワードさせます。 色々やりかたはあると思いますが、今回はMacにnginxを入れて、8080ポートをListenしてlocalhost:8080へforwardさせることにしました。
Macにnginxを入れる
==> Downloading https://homebrew.bintray.com/bottles/nginx-1.6.2.yosemite.bottle.1.tar.gz ######################################################################## 100.0% ==> Pouring nginx-1.6.2.yosemite.bottle.1.tar.gz ==> Caveats Docroot is: /usr/local/var/www The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that nginx can run without sudo. To have launchd start nginx at login: ln -sfv /usr/local/opt/nginx/*.plist ~/Library/LaunchAgents Then to load nginx now: launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist Or, if you don't want/need launchctl, you can just run: nginx WARNING: launchctl will fail when run under tmux. ==> Summary 🍺 /usr/local/Cellar/nginx/1.6.2: 7 files, 920K
configファイル確認
mac$ nginx -t nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
リバースプロキシとしてlocalhostの8080番へフォワード
// 略 location / { proxy_pass http://127.0.0.1:8080; } }
最終的にはこんな感じ。
iPhone [192.168.11.3] || ルータ [192.168.11.1] || [192.168.11.2] 192.168.11.2:8080 <----- nginx listen Mac || <------ nginx forward [192.168.59.3] 127.0.0.1:8080 || || <------ virtualbox forward [192.168.59.103] 192.168.59.103:8080 VirtualBox
これで、iPhone実機から192.168.11.2:8080
にアクセスすると見えるようになりました。
試した時はDHCPなので、安定稼働させるにはIPを固定化するとか、内部DNS立てるかすればよいと思います。
「webエンジニアでも出来る!iOSアプリのリファクタリングの全て」という勉強会に行ってきました
最近勉強会に顔を出したりしてますが、忘れないうちにメモを。
zigexn.co.jp 株式会社じげんの具志堅さんが掲題の講演されていました。
プロジェクトをどう管理しているか?
- ガントチャートはgantterつかってる(Google Chrome ext)
- プランニングポーカーしてる
- スクラムKPTやってる
- バーンダウンチャートつかってる(ホワイトボード)
- 「Apple審査 じげん」でググるとスライドあるよ
見つけたので貼っておきます。
www.slideshare.net
課題と背景
- 非常に短期間 & 少人数で進めていたのでオレオレコードが多くなっていた
- スケールするためにはどうしたらよいかを考えてみた
改善前
- ViewController整理されてない
- Controllerの肥大化 (あるある
- 初期化処理多すぎる
- ディレクトリがXcodeデフォ構成に引っ張られすぎた
- 型宣言などの冗長な記述が多い
- Array/Dictionary混在
- Dev/Prod環境の分離ができてない
やったこと
コーディング規約を用意した
- これを参考に独自に改変しつつ利用した
- Xcodeで一括置換して対応した
Info.plist / Build Settings を活用
- Info.plistに定数を定義
- Build Settings の User-Defined 以下に Debug/Releaseを分けて設定した
- DebugビルドとReleaseビルドを分けられるように
MVC構成
- グループに分けた (Models、ViewControllers//ViewController)
- Modelは状態の保持、加工のみの役割にした
- ViewControllerはModelへ加工の指示や取得を行い表現する
Model例
- ImageManager.swiftというモデルにし画像処理を行う
ViewController例
- Modelに処理を委譲して結果を表示するのみ
このあたり、クラス名がHogeManagerとかになっていたので厳密にはModelというよりServiceも兼ねていた模様。 なのでMVCというよりも、とにかくViewControllerからのロジック分離を目指して
- ViewController(VC)
- ロジック
のように2分割したと理解した。
デザインパターンを活用した
- Singleton
- Google Analytics を Adapter で利用
まあこのへんは、ですよねっていう感じで
技術的負債
- 抱えきれなくなる前に少しずつ解消していこう
Q&A
Swiftだから出来たリファクタリングはなにかあるか
- Objective-Cよりデザパタ適用しやすかったかなと。
規約をカスタマイズした過程を知りたい
- リードエンジニアが叩きを作ってエンジニアで揉んだ