スマートコントラクトの運営主体

いまスマートコントラクトは、0xやbancorとかもそうだが、ICOをしたプロジェクトが書いたものや、よくわからないがどこかの団体(a16zあたりから出資を受けていたりする)が書いたものが展開されている。

2017年、スマートコントラクトが注目され始めた当初からこの構造はできていたから不思議に感じていなかったが、よく考えるとこれはちょっと違和感がある話だ。

分散性が重要であるという議論から出発したブロックチェーンのはずが、特定の企業・組織・個人が運営するスマートコントラクトで処理が行われて、データはユーザーでも分散コミュニティでもよくわからないところに帰属していてプライバシーは存在しない。

このあたりにチャンスがあるように感じた。

0xの新バージョン(v2)の紹介

0xのソースコードを見ていたらcurrent, previousで大きく分かれる構成となっていた。調べたところ、0xは今夏メジャーアップデートを控えており、これまでの0xが抱えていた技術的な制約を取り払っていくという。

blog.0xproject.com

開発者ブログがあったので翻訳してみた。

0xプロトコル v2の紹介

サマリー

今日のエコシステム開発

0xプロトコルが実装された最初のスマートコントラクトは2017年8月にメインネットに公開された。以降0xコアチームはコミュニティからの好評に驚かされた。現在、15の0xリレイヤーがメインネットでERC20トークンを取引していて、dY/dXやSetのように新しい種類のアセット(例えばデリバティブやバスケットなど)を扱うプロトコルを0x上で開発しているチームもある。

f:id:devtokyo:20180616000742p:plain

エコシステムが驚くほどユーザーに受け入れられていることも重要だ。以下ハイライト。

f:id:devtokyo:20180616000840p:plain

延トレード数・・・10万以上
出来高・・・183,000,000ドル(約200億円)
24h出来高・・・4,000,000ドル(約4億円)
週次成長率・・・10%
扱ったトークンの種類・・・300以上

(訳者コメント: とはいえ中央集権取引所の出来高に比べると上の数字ではまだまだマイナーの域に過ぎない)


1日ごとの出来高を時系列にすると取引高の大きな成長が容易に見て取れる。

f:id:devtokyo:20180616000906p:plain

私たちは開発者コミュニティと深い関係を築き、オープンソースのツールを作るためにフィードバックを活かした。それらは0xを使って開発するチーム、ならびにイーサリアム全体のエコシステムのためだ。

組織としては14人のチームに成長し、年内は拡大を続ける予定である。

0xプロトコルの価値は、開発者コミュニティの貢献(例えばOpenRelayのチームによる0xtracker.comはすばらしいプロジェクトだ)によって拡大している。彼らのような情熱を持った開発者・チームなしには我々は成立し得ない。

0x v2について

アセットのトークン化(The tokenization of assets)はすでに始まった。この流れは今後加速度的に続くだろう。私たちがv1をローンチしたときちょうど最初のERC20トークンが誕生し始めたところだった。それからというものERC20のエコシステムは爆発的に広がり、ERC721、セキュリティトークン、デリバティブなどの新しいアセットも出現した。我々の主題である 世界の価値はトークン化 に合わせて、我々は価値が摩擦なくやりとりされるためのツールを作っている。

フレキシブル・モジュラー志向・容易なアップグレードを我々は重視している。
フィードバックを尊重し、ZEIP(zerox improvement proposal)に寄せられた提案の多くを実装している。

新しいコントラクトアーキテクチャはERC721を含むさまざまなトークン規格をサポートしていく。

バージョン1では中心的な役割のコントラクトが2つあった。Exchangeコントラクト はオーダーの発注・キャンセルのロジックを担当し、Proxyコントラクト はトレードが実行されたときのERC20のトークン量を変更するインターフェースとなる(訳注 ERC20は所有者が他のコントラクト、つまりここでは0xに対して取引を認可する仕組みがある。approve()によってエンドユーザーが0xに対して操作の権限を与え、allowance()というview関数を参照すると実際に認可を得ているかが分かる。こういった認可の確認や実際の取引の処理の抽象化をProxyコントラクトが行う)。

これはワークしているものの、このままではERC20以外のアセットをサポートできない。というのはこのやり方では新しいトークン規格に対応するためには Proxyコントラクトを継続的にデプロイしなおす必要 があり、ユーザーはProxyコントラクトが新しくなるたびに認可を与え直す必要がある。

f:id:devtokyo:20180616000937p:plain

そこでv2では単一のProxyコントラクトと直接やりとりするのではなく、トークン規格ごとにProxyを用意する。この方法なら一つのProxyをデプロイしなおす必要なくトークン規格に対応していくことができる。

f:id:devtokyo:20180616001002p:plain

v2の開始時点ではERC20, ERC721の対応をローンチする。Ethmoji, Fan Bits, CryptoKitties, LAND、その他のデジタルコレクティブが0xでやりとりできるということだ。

この新しいモジュラーアーキテクチャは既存のスマートコントラクトに修正を加えたり、開発者・ユーザーにアップデートを強いることなくトークン規格に対応することを可能にした。

そのうちENS, ERC777トークン、R-Tokenも0xでやりとりできるだろう!

EIP-712のサポート

メッセージに署名するとき、そのメッセージが読めないことは苦痛じゃないか?
だから0xチームのメンバーはEIP712、構造化データのハッシュ化標準の提案を作り、それをv2で採用する。

以下のスクリーンショットでEIP712による署名がいかに有用かがわかるだろう。

f:id:devtokyo:20180616001019p:plain

Takerの抽象化

バージョン1では、注文のTakerは fillOrder関数を呼び出したmsg.senderと常に一致していた(訳注: つまり注文を出す人物とトークンの所有者が一致している必要があったので、トークン所有者が別のコントラクトに注文処理を委託するようなことができなかった)。
v2ではTakerはデフォルトでmsg.senderとなるがそのアドレスによる署名が提供されれば別のイーサリアムアドレスで行うことが可能になる。これは新しいユースケースを広げ、0xを利用した以下のような実装を容易にしてくれる。

リレイヤーによるOrder Matching Modelの利用

takerをホワイトリスト方式にしたり、マルチシグによる注文を要求したりできる。
Trade Execution Corrdinators(訳語不明、教えて下さい)をつくってフロントランニングや嫌がらせを防げる。

新しい署名方式のサポート

バージョン1では注文はイーサリアムの標準暗号スキームであるECDSAで署名して作られている。しかしこれには潜在的に制約がありユースケースを制限してしまう。

v2ではEIP712やTrezorのような新しい署名スキームを採用し、ユーザーに各自のスマートコントラクト内で独自の検証を定義することを可能にしている。これで注文をマルチシグやBLS署名、ring署名その他の暗号化方式で作ることが可能になり、新しいユースケースに対応するために0xのスマートコントラクトをデプロイし直す必要がなくなる。
スマートコントラクト自体が任意の署名検証によって0x上で注文を出すことが可能になったのだ。

アトミックなオーダーマッチング、バッチ処理(訳語間違ってるかも)

v1ではユーザーは一括で注文を出すことができるが前もってトランザクションのために原資を用意する必要があった。
この資金の壁を突破しないと、注文をマッチングするリレイヤーを作ったり、アービトラージbotを運用することができない。

v2では注文は最初のgasコストのETH以外の前金なしでアトミックにマッチング・約定する。
これは裁定取引やオーダーマッチングの障壁を大幅に低くする。

Forwarding Contract

ETHをERC20互換のWETHに変換する作業は一般ユーザーが0xを受け入れるハードルになっている。(訳注: 0xはERCトークンの交換規格のため、ETHそのものを直接的に扱う術がなかった。solidity上はETHとトークンを区別して分岐して処理するとか、ETHを指すマジックナンバーとしてのアドレス, '0xeeeeee'のようなものを用意するなどすれば回避できるはず。KyberNetworkはその方式。)
ユーザーが0xを簡単に使う解決策を探したところ、”forwarding contract” と “trade widget”(訳語不明) の可能性にたどり着いた。

“Forwarding Contract” では、ユーザーはETHと注文をただ一緒に送ればよく、”forwarding contract”が1つのトランザクションの中でETHをwrapして注文を出すことでTakerはWETHが必要なくなる。

v2では誰でも使える我々のバージョンの”forwarding contract”をtrade widgetと一緒もしくは単独で提供する。
これはもちろんオープンソースで誰でも改変したコントラクトを作ってデプロイして良い。

f:id:devtokyo:20180616001043p:plain

Launch Timeline

5/21・・・v2のインターフェースの最終版を提示
6/9・・・kovanにデプロイ、既存のリレイヤーやdappsと統合テストを実施
7/2・・・DiligenceとQuantstampによるセキュリティ監査の開始
7/30・・・メインネットへデプロイ

リレイヤーのアップデート手順

省略

ユーザーのアップデート手順

v1で作った注文は無効になりv2では処理されない。ただしv1ではそのまま有効となるので約定させたくなければv1に出した注文をキャンセルするか、手持ちのERC20のallowanceを解除すればいい。
ユーザーはv2のProxyコントラクトにallowanceを行う必要がある。

WETHはどちらのバージョンの0xでも利用可能。
v2のローンチと同時にポータルサイトのアップデートも行うので0xリレイヤー、dappsを見つけることができる。

FAQ

v1のスマートコントラクトはどうなるか

v1のコントラクトはv2がローンチされリレイヤー、ユーザーが新しいコントラクトに移行できるまでしばらく有効となる。しかしExchangeコントラクトからProxyコントラクトへのアクセスはMultiSigWalletを使って無効化される。v1の廃止勧告は時期が近づいたらアップデートするが、10月の終わりを目処にしている。

v2のガバナンスについて

Governance in 0x Protocolで説明したように、我々は完全な分散型ガバナンスモデルへの移行を検討している。TCRを開始し、コミュニティの拒否権を近々組み込んでいく。
Proxyコントラクトをコントロールする期限付きのMultiSigはv2でも存在するが段階的に廃止予定。

Standard Relayer APIはv2にいつ更新されるか

APIの更新は現在策定中でv2のデータ型に準拠したものになる。現在のAPIエンドポイントは引き続き有効。

ERC20, ERC721以外のTokenProxyがデプロイされるのはいつか

それらに関して確かなスケジュールは決まってないが、計画に則って優先的に行っていく。

ユーザーとしてv2でトレードするのに何をすればいいか

ユーザーはただV2のProxyがトレードを実行するためのallowanceを行えば良い。v1の注文はそのまま有効なのでそれらをキャンセルしたければ手動でキャンセルするか、v1のProxyコントラクトに対してallowanceを0にすればいい。

感想

以上みたように0x上ではすでに様々な実用的なサービスから意欲的なプロトコルまで開発がなされ、v2では大きく進化しようとしている。とくに個人的にはコントラクトが0xに注文を出せるようになる点に注目したい。
夏以降0xを中心に様々なプロトコルが立ち上がり、どのようなプレイヤーが存在し自分がどこにポジションをとるべきか。ディベロッパーとしては想像力と技術理解を膨らませて粛々と用意するだけでしょう。

日本でスタートアップがブロックチェーンを事業にするのはきわめて難しいという説

要約

ブロックチェーンのサービスをやるスタートアップを作ろうとしているけど、法律とか規制的になかなか難しいよね、という話。

筆者について

ブロックチェーンで何か面白いサービスができないかと考えているエンジニア。python、React、solidityあたりを書いている。 本題を取り上げる前に、ブロックチェーンをサービスにするというのがどういうことか、整理する。

暗号通貨あってのブロックチェーンである

「仮想通貨はバブルだが、ブロックチェーン技術は本物」という主張があるが、それには反対である。というのは、ブロックチェーンはルールに基づいて、誰でもブロックチェーンを使ったり、貢献したりできる。ビットコインの場合は使う(送金する)ときは手数料がかかり、トランザクションの検証や新しいブロックの採掘に協力したマイナーは報酬としてBTCを手にできる。 分散型のネットワークが巧みなインセンティブ設計によって協調することを可能にしたのがブロックチェーンということになる。 ブロックチェーンをデータベースとして見た場合、保存効率やレイテンシ、一貫性などの点で普通のRDBMSや分散型データベースのほうが圧倒的に使い勝手が良い。台帳とかスマートコントラクトという言葉に惑わされて普通の帳簿システムとか文書類を載せようとしても徒労に終わるだろう。

ブロックチェーンの革新はプロトコル開発をマネタイズできるようになった点にある

プロトコルとは実アプリケーションの下に位置する、共通の規格のこと。例えばウェブにおいてはTCP/IPやHTTPがプロトコル(括りが大きいけど)、その上に表現されてるFacebookとかAmazonみたいなWebサービスはアプリケーションということになる。 いままでインターネットの歴史はアプリケーションを開発してデータを独占することが収益化の方法だった。プロトコルを策定して実装することは、一部のギークが無償で奉仕したり、巨大化したインターネット企業がスポンサーとなって財団を立ち上げることによって成り立っており、プロトコル開発そのものをマネタイズする方法はなかった。 ブロックチェーンにおいては初期開発者がトークンを作り、プロトコル上でそのトークンが意味を持つように設計することでトークン価値が上昇し、開発者が大金を手にすることができる。イーサリアムがその例であり、イーサリアム上では債券のプロトコルや融資のプロトコルなどが生まれていっている。

ICOは確かに問題が多いが、トークンを発行してほしい人が買うこと自体に問題はない

ICOはERCトークンを多少いじってデプロイしてしまえば誰でも簡単にできるので、詐欺が多いのが実情。またよくわからず大言壮語している開発者も多いだろうから、結果的にホワイトペーパーに書いたことが実現できないことも十分起こりうる。 しかし全くの嘘でお金を集めているなら、それはICOという前に詐欺罪にあたるはずだし、ICOの健全性を検証するようなサイトも増えてきている。

制度・規制の問題点

1. 仮想通貨交換業の適用範囲が広いこと

仮想通貨交換業は①法定通貨と仮想通貨を交換する事業 ②ビットコインなど主要な仮想通貨とマイナーな仮想通貨を交換する事業 に必要となる。また取り扱える仮想通貨も厳密に決まっている。 これはユーザーの資産を預かる企業が適切な規制下で内部統制や資産管理を行うこと、投資家保護が立法趣旨かと思う。 それ自体は概ね同意なのだが、「ウォレットアプリを提供して、片方のユーザがビットコインを送ると、受け取ったユーザーは円とかそのほか好きな通貨として受け取れる」というような機能をつけるにも交換業が必要になる。 また、DEXと呼ばれるサービスを提供するのもアウトになると思われる。DEXとはユーザーの資産を預からず、ブラウザに秘密鍵を持たせた状態で通信させる取引所である。詳しくは次の記事を参照。

zoom-blc.com

エコシステムとしてこのようなサービスがどんどん出てきて手数料やユーザビリティの面で競争するのが望ましいと思う。

アイテムやキャラクターがトークン化されたゲームもグレーである。ゲームアイテムであってもトークンには変わりないのだから定義としてはしょうがないのだけど、法律がこういうものの規制まで念頭にしていたのかは不明である。

ちなみに「海外からの旅行客に対して日本の個人がツアーコンダクターとなって、案内をしたり運転したり買い物をしてやって、その日の支払いをビットコインで清算するサービス」とか面白いのではと思ったけど、これをやるにはツアーコンダクターたる個人に仮想通貨交換業をとってもらう必要がある。

2. 仮想通貨交換業をとるハードルが高くなっている

たしかに無許可の取引所が乱立したらそれはそれで問題が生じるのは容易に想像できる。それならちゃんと交換業をとればいいよねという話だけど、今後ベンチャーが交換業をとるのは実質不可能だと思われる。 噂ベースだが金融庁の担当者が一人とか数名とかとにかく少ないらしい。審査項目の内部統制も、上場企業レベルのものが求められると聞く。 職業紹介業とか飲食店の営業許可くらいカジュアルにとれればいいと思うのだけど。

3. ICOができない

上述の通り取り扱える仮想通貨がきまっているからICOをするには交換業を取得し新規発行する通貨について申請する必要がある。つまり資金需要があるプロジェクトが主体となるのではなく、今後は適当なICOプラットフォーム事業者に上場申請するような形式になるかと予想できる(そしてそれはSBIやテックビューロがその地位を狙っている)。 ICOはアメリカでもダメ?なはずだけど、ばんばん有望なプロジェクトがICOしている。英語なら日本人もやっていいのかというと、そのへんも微妙そうである。

4. 業界団体が利権化しつつある

既存の仮想通貨交換業者が業界団体を作っているが、動向を見てるとどうも規制を強める方向に動いているようである。 新規に交換業をとれるのはLINEとかメルカリとか、あるいはゴールドマンサックスとか、彼らが逆らえないような勢力だけで、そのへんのスタートアップは握りつぶされるだろう。

5. 税金

これはよく言われることだから簡単に済ますけど、増えたビットコインを利確する(円転する)、別の仮想通貨に変える、商品やサービスの対価に使用する時点で購入時の価格と時価の差で課税される。この「別の仮想通貨に変える」ときの課税が厄介で、せっかくビットコイン長者が生まれても別のクリプトに資金が還流していかない。 ちなみに商品やサービスの対価に使用する点だけど、確かドイツでは非課税らしい。

事業化できる分野

今後ビットコインブロックチェーンを事業にするなら、

ブロックチェーンエンジニアの教育コンテンツ・研修・派遣 ②ブロックチェーンの分析サービス ③税金の計算 ④仮想通貨のメディア ⑤謎なブロックチェーンの実証実験 ⑥中央集権サービスへの仮想通貨の組み込み(広告みるとビットコインもらえるとか)

あたりは問題なさそうだが、真にブロックチェーンの思想を体現するようなサービスで革新を起こすのは難しそう。まぁでも国民全員でBTCをガチホしてるだけでいいのかもね。

docker でGAE + Python2.7環境を構築し、ついでにCircleCI2.0で動かす

なぜdockerを使うのか

GAEのStandard EnvironmentsではPython2.7を使うことでインフラ環境をセットアップするコストなく稼働するサービスをすぐに立ち上げられます。また展開されたアプリケーションはトラフィックの量に応じて自動でサーバの台数(インスタンス)が増えたり減ったりするので、サービスの可用性を担保するのに必要な開発・運用コストもがくっと小さいのが特徴です。

Docker Containerとして振る舞うGAE Flexible Environmentsはdockerの設定などを意識する必要がありますが、Standardでは基本的に不要です。 しかし、以下のような都合で今回dockerを用意することにしました。

1. Circle CIへの対応

Circle CIは1.0と2.0があり、2.0は標準でdockerの設定を使えるのが特徴です。逆にいうと利用にはdockerでサーバ構築手順をコード化することが必須です。

2. windowsを使う同僚が入った

私は開発ではMacBookを利用していますが、同僚がwindowsであったため、windowsmacで環境構築方法を分けるのもモダンでない気がしたので、この際書いてみました。

実際にやってみて

dockerを体系的に実務で使ったのは初めてでしたが(いままで誰かが書いたdocker-composeを引き継いでコマンドだけ動かす程度はやっていました)、概念を理解すれば思った以上に簡単で便利でした。 デバッグ時にREPLを立ち上げるとdocker環境が挟まることで入力時の補完が効かない(Ctrl + R, ↑とか) とかがあったものの、概ね快適です。

作業手順

appengineのDockerfileを作る

まずappengine環境(python + GCPのライブラリ群)が入ったイメージを作っていきます。Dockerfileを書きます。

.docker/appengine/Dockerfile の中身が以下。

FROM debian:jessie-slim
LABEL mantainer "DevTokyo, Inc"

ENV PATH=/usr/lib/google-cloud-sdk/bin:/usr/lib/google-cloud-sdk/platform/google_appengine:$PATH
ENV PYTHONPATH=$PYTHONPATH:/usr/local/lib/python2.7/site-packages
RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ARG APT_MIRROR=httpredir.debian.org
RUN sed -ri "s/(deb|httpredir).debian.org/${APT_MIRROR}/g" /etc/apt/sources.list
RUN apt-get -y update && apt-get -y upgrade
RUN apt-get install -yqq --no-install-suggests \
  curl \
  gcc \
  git \
  libc6-dev \
  make \
  openssh-client \
  build-essential \
  python-setuptools \
  libatlas-base-dev \
  libssl-dev \
  gfortran \
  libffi-dev \
  libxml2-dev \
  libxslt1-dev \
  zlib1g-dev \
  libpng-dev \
  libfreetype6-dev \
  ncurses-dev \
  libncurses5-dev \
  g++ \
  sqlite3 \
  libsqlite3-dev \
  libhdf5-dev \
  python-lxml \
  unzip && \
  rm -rf /var/lib/apt/lists/*

ENV PYTHON_VERSION 2.7.12

RUN set -ex \
  && curl -fSL "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz" -o python.tar.xz \
  && curl -fSL "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz.asc" -o python.tar.xz.asc \
  && mkdir -p /usr/src/python \
  && tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz \
  && rm python.tar.xz* \
  && cd /usr/src/python \
  && ./configure --enable-shared --enable-unicode=ucs4 \
  && make -j$(nproc) \
  && make install \
  && ldconfig \
  && curl -fSL 'https://bootstrap.pypa.io/get-pip.py' | python2 \
  && pip install --no-cache-dir --upgrade \
  && find /usr/local \
  \( -type d -a -name test -o -name tests \) \
  -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \
  -exec rm -rf '{}' + \
  && rm -rf /usr/src/python


RUN curl https://sdk.cloud.google.com | bash -s -- --disable-prompts --install-dir=/usr/lib && \
  gcloud config set core/disable_usage_reporting true && \
  gcloud config set component_manager/disable_update_check true && \
  gcloud config set metrics/environment github_docker_image && \
  \
  gcloud components install \
                    app-engine-python \
                    beta \
                    app-engine-python-extras
RUN chmod +x \
  /usr/lib/google-cloud-sdk/platform/google_appengine/dev_appserver.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/remote_api_shell.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/appcfg.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/backends_conversion.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/bulkload_client.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/bulkloader.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/download_appstats.py \
  /usr/lib/google-cloud-sdk/platform/google_appengine/endpointscfg.py

ENV APP_HOME /devtokyo
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
VOLUME $APP_HOME

Dockerfileは少し書いたら

$ docker build .docker/appengine -t test

とすればちゃんとビルドできるかテストできます。Dockerfileの中身を解析してSTEPごとに実行していくので、すでにパスしたSTEPであればキャッシュを利用して高速にビルドできます。なのであまり変更がないものを上にもって来ると良いでしょう。

docker-compose.ymlを作る

docker-composeはDockerのプロセスをまとめて管理してくれるサービスです。典型的にはRuby(Rails)のサーバのプロセス、MySQLのプロセス、memcached、nodeなど、ウェブサービス開発で一通り必要なプロセスを分割統治できるようになります。 詳しく知らないのですがDockerはミドルウェアごとにプロセスを立ち上げ、それをまとめて管理するのが良い設計のようです。そうすることで、ミドルウェアごとの依存関係を考慮せずに済み、memcacheやnodeなどそれぞれのイメージを公式が配布しているものなど好きなように選択できます。

今回、nodeのサービスを追加してdocker-compose.ymlを作りました。

version: '3'
services:
  appengine:
    build:
      context: ./
      dockerfile: .docker/appengine/Dockerfile
    command: bash -c "dev_appserver.py front.local.yaml admin.local.yaml task.local.yaml --host=0.0.0.0 --enable_host_checking=false --datastore_path=data/datastore"
    stdin_open: true
    tty: true
    ports:
      - "8080:8080"
      - "8081:8081"
      - "8082:8082"
      - "8000:8000"
    volumes:
      - .:/devtokyo
      - lib:/devtokyo/lib
    container_name: appengine
    image: appengine

  node:
    build:
      context: ./
      dockerfile: .docker/node/Dockerfile
    command: yarn start
    volumes:
      - .:/devtokyo
      - node_modules:/devtokyo/node_modules
    ports:
      - "4001:4001"
    container_name: node
    image: node

  go:
    build:
      context: ./
      dockerfile: .docker/go/Dockerfile
    command: go run main.go
    volumes:
      - .:/devtokyo

volumes:
  lib:
    driver: local
  node_modules:
    driver: local

これであとは

$ docker-compose run --rm node yarn start
$ docker-compose run --rm appengine pip install -t lib -r requirements.txt

$ docker-compose up -d

のように各サービスごとにコマンドを実行したり、サービスをまとめて展開できます。

circleCIの設定

circleCIでは .circle/config.yml というファイルを作ります。

version: 2
jobs:
  build:
    machine: true
    working_directory: ~/devtokyo
    steps:
      - checkout
      - restore_cache:
          key: docker-{{ checksum "docker-compose.yml" }}-{{ checksum ".docker/appengine/Dockerfile" }}
          paths: ~/caches/images.tar
      - run:
          name: Check cache file, if not exists then pull images and generate cache.
          command: |
            if [ ! -f ~/caches/images.tar ]; then
              docker-compose build appengine
              mkdir -p ~/caches
              docker save -o ~/caches/images.tar $(docker history -q appengine | tail -n +2 | grep -v \<missing\> | tr '\n' ' ')
            else
              docker load -i ~/caches/images.tar
            fi
      - run: docker-compose build appengine
      - save_cache:
          key: docker-{{ checksum "docker-compose.yml" }}-{{ checksum ".docker/appengine/Dockerfile" }}
          paths: ~/caches/images.tar
      - restore_cache:
          key: pip-{{ checksum "requirements.txt" }}
          paths: ~/caches/pip.tar
      - run:
          name: pip install
          command: |
            if [ ! -f ~/caches/pip.tar ]; then
              docker-compose run appengine pip install -t lib -r requirements.txt
              # 圧縮
              tar -cvf ~/caches/pip.tar -C ~/devtokyo lib
            else
              # 解凍
              tar -xvf ~/caches/pip.tar -C ~/devtokyo/
            fi
      - run:
          name: put service account file
          command: |
            mkdir -p ~/devtokyo/cert
            echo $SERVICE_ACCOUNT_DEV | base64 -d > ~/devtokyo/cert/devtokyo-dev.json
      - run:
          name: Run tests
          command: docker-compose run appengine python -m unittest discover

いろいろありますが上から見てきます。 まずcircleCIはこのファイルを見て、上からステップを実行していきます。 restore_cache は文字通り、キャッシュがあればそれを復元します。キャッシュは任意に作れますが、一番はDockerのビルドをキャッシュすることです。これがあるのとないので、毎回Dockerのビルドが必要になってしまうと5,6分は余計にかかってしまいます。 もちろんDockerfileやdocker-composeに変更があったときはキャッシュを捨ててほしいので、 circleCI側で提供している checksum という関数にファイル名を渡してやります。これでファイルに変更があった際にキャッシュのキーが変わるようです。 cacheが読み込めていれば、 docker-compose build appengine は高速に終了します。

次に時間がかかるのはpipライブラリのインストールです。これもキャッシュがないとけっこう時間がかかります。本当は -t lib を使わなければ requirements.txt の変更分だけをインストールすることができるようなのですが、tオプションがある場合だとpipコマンドがうまく対応できないみたいです。なのでrequirements.txt をキャッシュのキーにして、変更がない際はスキップするようにしました。

次に、以下のようなコードでサービスアカウントを書き込んでいます。

mkdir -p ~/devtokyo/cert
 echo $SERVICE_ACCOUNT_DEV | base64 -d > ~/devtokyo/cert/devtokyo-dev.json

これはサービスアカウントの鍵ですが、アカウントの権限によっては強力な権限を持っているわけなので、一般的にはレポジトリから除外します。今回実装しているサービスではこの鍵を使ってBigQueryに接続する部分があり、それがテスト環境でも必要だったので環境変数から鍵のファイルを作っています。

このへんを参考にしました。 medium.com

で、ようやく最後にテストを実行するコマンドがあります。

command: docker-compose run appengine python -m unittest discover

まとめ

一見するとハードルは高いようですが、Dockerfileの作成から地道に少しずつやってみると意外と簡単でした。あまり知識がない状態からでも2日程度で済みました。 インフラのコード化とかテストとか、開発のための開発という気がしていて避けていたのですが、Dockerは初日から開発効率をあげられるものだと感じたし、今後コンテナ化は不可逆的な流れだと感じています。