猫でもわかる rkt + Kubernetes
このエントリは Kubernetes Advent Calendar 2017 の 23 日目の記事です。ちなみに昨日は takezaki さんの「GCBを利用したContinuous Delivery環境」でした。
LT で使用したスライド
先日、市ヶ谷Geek★Night #16 の 10 分 LT 枠で、CoreOS 社によるコンテナ実装 rkt とその Kubernetes 連携について発表してきました。今回のエントリはこの LT の内容を補足しつつ、実際に手を動かして rkt を試せるような構成にしてあります。
Hello, rkt!
rkt は、Docker の対抗馬として CoreOS によって開発されたコンテナ管理ツールです。プロジェクトの初期は普通に Rocket という名前でコマンド名が rkt
という扱いでしたが、途中からツールの名前自体が rkt に変更されました。
現在は Cloud Native Computing Foundation (CNCF) にプロジェクトごと寄贈されていますが、メンテナ を確認すると 11 人中 8 人が CoreOS 所属で、実際には依然として CoreOS が中心となっているようです。ちなみに残りの 3 人は Kinvolk 社 所属で、後で登場する rktlet の開発元でもあります。
rkt のインストール
Go で作られたツールの常として、rkt もインストールは簡単です。
Linux の場合、各ディストリビューション向けのパッケージ配布もされていますが、触ってみるだけであれば直接バイナリをダウンロードすればすぐ試せます。
この記事の後半で rkt app
コマンドを使用するためには現時点の最新版である v1.29.0
が必要なのでそこだけ注意してください。
$ wget https://github.com/rkt/rkt/releases/download/v1.29.0/rkt-v1.29.0.tar.gz $ tar xzvf rkt-v1.29.0.tar.gz $ sudo mv rkt-v1.29.0/rkt /usr/local/bin/rkt $ rkt version rkt Version: 1.29.0 appc Version: 0.8.11 Go Version: go1.8.3 Go OS/Arch: linux/amd64 Features: -TPM +SDJOURNAL
残念ながら rkt は Linux 専用です。rkt のレポジトリが Vagrantfile を提供しているので、Mac や Windows の人はこれで VM を立ち上げれば大丈夫です。
git clone https://github.com/rkt/rkt cd rkt vagrant up
特徴その1:デーモンが存在しない
Docker の場合、docker
コマンドを使い始める前に Docker Engine をサービスとして登録・起動する作業(実際にはパッケージマネージャがやってくれると思いますが)が必要です。
rkt の場合 Docker Engine に相当するコンポーネントがないため、ダウンロードしたバイナリだけでツールとして完結しています。 実際にちょっと触ってみましょう。
Docker と少しだけ異なる点として、rkt ではイメージに署名が付けられています。信頼した公開鍵は /etc/rkt/trustedkeys
以下に記録されています。
$ sudo rkt trust --prefix coreos.com/etcd ... $ tree /etc/rkt/trustedkeys/ /etc/rkt/trustedkeys/ └── prefix.d └── coreos.com └── etcd ├── 18ad5014c99ef7e3ba5f6ce950bdd3e0fc8a365e └── 8b86de38890ddb7291867b025210bd8888182190
あとの操作は Docker とほぼ同じです。イメージをダウンロードしてからコンテナを立ち上げて消すまでの一連の流れを試してみましょう。
$ sudo rkt fetch coreos.com/etcd:v3.1.7 ... image: signature verified: CoreOS Application Signing Key <security@coreos.com> ... $ sudo rkt image list ID NAME SIZE IMPORT TIME LAST USED sha512-e7a54697d04d coreos.com/etcd:v3.1.7 58MiB 43 seconds ago 43 seconds ago $ sudo rkt run coreos.com/etcd:v3.1.7 (別ターミナルで) $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS a79a0bd6 etcd coreos.com/etcd:v3.1.7 running 1 minute ago 1 minute ago default:ip4=172.16.28.2 $ sudo rkt stop a79a0bd6 "a79a0bd6-4260-479d-993d-08f032781007" $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS a79a0bd6 etcd coreos.com/etcd:v3.1.7 exited 12 minutes ago 12 minutes ago $ sudo rkt rm a79a0bd6 "a79a0bd6-4260-479d-993d-08f032781007" $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS
Docker Hub にホストされているイメージを立ち上げることも可能で、イメージの指定は docker://IMAGE:TAG
という形式になります。この場合は署名の検証ができないので --insecure-options=image
を付けます。
$ sudo rkt run --insecure-options=image docker://redis:4.0.6 $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS 2df6d5ee redis registry-1.docker.io/library/redis:4.0.6 running 11 seconds ago 11 seconds ago default:ip4=172.16.28.2 (以下同様)
特徴その2:Pod が最初からサポートされている
もう一つ、Docker に対する rkt の特徴として挙げられるのは Pod のサポートです。上の例ではコンテナが一つしかないのではっきりとわかりませんが、実は UUID
はコンテナではなく Pod に対して振られています。
あまり意味のある組み合わせではありませんが、先ほど fetch
した etcd
と redis
を同じ Pod 内で起動させてみましょう。
$ sudo rkt run coreos.com/etcd:v3.1.7 docker://redis:4.0.6 (別ターミナルで) $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS 1f752f5c etcd coreos.com/etcd:v3.1.7 running 8 seconds ago 8 seconds ago default:ip4=172.16.28.2 redis registry-1.docker.io/library/redis:4.0.6
Pod 1f752f5c
の内部で etcd
と redis
が立ち上がっている様子がわかります。
この二つのコンテナは、systemd の機能によって他から隔離されています。例えば以下のように ip netns list
コマンドを実行してみましょう。Pod に対応する network namespace が作成されているのがわかるはずです。また、Pod を rm
すると namespace も消えます。
$ ip netns list cni-e0f06652-1a94-2cfd-3ccc-c684c473992f (id: 1) $ sudo rkt stop 1f752f5c "1f752f5c-6046-4458-8105-832227a07cfa" $ sudo rkt rm 1f752f5c "1f752f5c-6046-4458-8105-832227a07cfa" $ ip netns list (出力なし)
さて、ここでもう一度イメージの一覧を表示してみましょう。
$ sudo rkt image list ID NAME SIZE IMPORT TIME LAST USED sha512-e7a54697d04d coreos.com/etcd:v3.1.7 58MiB 1 hour ago 1 hour ago sha512-e50b77423452 coreos.com/rkt/stage1-coreos:1.29.0 211MiB 1 hour ago 1 hour ago sha512-eb03c7ec1861 registry-1.docker.io/library/redis:4.0.6 204MiB 1 hour ago 1 hour ago
立ち上げた覚えのないイメージ coreos.com/rkt/stage1-coreos:1.29.0
がいつの間にか fetch
されていますね。このイメージこそ、rkt がネイティブで Pod をサポートできる仕掛けです。
rkt のアーキテクチャでは、コンポーネントは三つの役割に階層化されています。先ほど rkt
コマンドを操作した際は目立ちませんでしたが、ユーザの操作とアプリケーションコンテナの起動の間に Stage1 が入っているのがポイントです。Stage1 は Pod の実装を提供し、Pod 内のコンテナを他のプロセスから隔離します。
- Stage0:ユーザと直接やりとりする
- Stage1:Pod の実装を提供する。Stage0 から呼び出される
- Stage2:コンテナ化されたアプリケーション本体。Stage1 から呼び出される
ここで面白いのは、Stage1 を切り替えることで、Pod ごとに隔離レベルを変えられる点です。rkt は以下の三つの実装を公式に提供しています 。
- stage1-coreos:デフォルトの設定。systemd の namespace および cgroup を使用して隔離する
- stage1-fly:より隔離レベルが低い設定。chroot でファイルシステムのみを切り替え、namespace は隔離しない
- stage1-kvm:より隔離レベルが高い設定。KVM を使用して Pod ごとに仮想マシンを作成し隔離する
使用する Stage1 の実装は、--stage1-name
オプションで指定することができます。試しに fly による Pod 作成を試してみましょう。
fly による Pod を作成する場合、イメージ側で定義された volume
に対して自動的にマウントポイントをつくる仕組みがうまく動作しないようです。ここでは --volume
オプションで明示的に指定して起動させました。
$ sudo mkdir /mnt/etcd-data $ sudo rkt run --stage1-name=coreos.com/rkt/stage1-fly:1.29.0 --volume data-dir,kind=host,source=/mnt/etcd-data coreos.com/etcd:v3.1.7 (別ターミナルで) $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS 307f689f etcd coreos.com/etcd:v3.1.7 running 16 seconds ago 16 seconds ago
先ほどの場合と異なり、NETWORK
欄が空のままになっていることに注目しましょう。これは network namespace が隔離されていないことによるもので、実際、Pod が running の状態にもかかわらず ip netns list
コマンドを実行しても何も返ってきません。
Container Runtime Interface
rktnetes
さて、ここまで見てきたように、rkt には次のような特徴がありました。
- Docker と違ってデーモンがなく、Linux 付属の systemd を使用
- 複数のコンテナを Pod としてまとめて隔離する仕組みを持っている
これらの性質を見ると、いかにも Kubernetes(あるいはそれ以外のオーケストレータも)との相性が良さそうに見えます。
実際、rkt 側はもちろん Kubernetes 自身もそう考えていたようで、Kubernetes v1.3、Docker 以外の初のコンテナランタイムとして rkt 連携がサポートされました。時系列としては 2016 年 7 月のことです。
この rkt 連携機能は rktnetes と通称されていました。当時の Kubernetes 公式ブログ の表現を引用しましょう。
rktnetes is about more than just rkt. It’s also about refining and exercising Kubernetes interfaces, and paving the way for other modular runtimes in the future.
すなわち、rktnetes は Kubernetes とコンテナランタイムとの間のインターフェースを設計する上での先行事例として位置づけられていたことがわかります。
しかしその目的に反して、実装の中身はかなり乱暴です。Kubernetes のレポジトリ内、kubernetes/pkg/kubelet/rkt
にある ソースコード を見ると、インタフェースを作ったというよりは、rkt のデータ構造や動作を Kubelet 側でも実装した、と表現したほうが近いように見えます。
CRI の登場
Kubernetes v1.5 でまた別の動きが現れます。Container Runtime Interface (CRI) の策定です。
CRI が定義する API は .proto
ファイル の形で提供されており、大きく分けて二つの gRPC サービスからできています。
前者のイメージの操作については、それぞれ CRI に対応するコマンドが rkt 側にももともと存在するので大きな障害にはなりません。
問題は後者のコンテナ操作です。CRI には
- Pod を作成する
- Pod 内にコンテナを作成する
- コンテナを起動する
といった逐次実行命令型のインタフェースが規定されています。しかし rkt の Pod は 基本的に起動後に状態を変更することができない、いわゆる immutable なつくりになっていて、そのままでは Pod の作成とコンテナの追加を独立して行うことができません。
CRI の策定に当たって Pod レベルの宣言的なインタフェースを避けた理由は、初期段階のプロポーザル で触れられています。
- すべてのランタイムが Pod をネイティブサポートしているとは限らず、対応負荷が大きくなるため
- インタフェースが抽象的すぎると、再起動や LifecycteHook のロジックを各ランタイム側で実装することになり重複が生じるため
- 開発速度の速さから、Pod の設定項目はすぐ変化する可能性があるため
後者二つはともかく、最初の要件は rkt 側からすれば完全に梯子を外された形です。
ちなみに、もちろん Docker はいち早く CRI に反応しました。それまで Kubernetes が Docker を呼び出すコードは kubernetes/pkg/kubelet/dockerutils
にありましたが、新たに CRI 対応版である kubernetes/pkg/kubelet/dockershim
が開発され、半年後である v1.7 の段階 では移行が完全に完了してdockerutils
は姿を消しています。
一方 rkt はと言えば、実は Docker 同じように v1.5 の段階ですでに rktshim
が 作成されて います。しかし中身を覗いてみると実装はなく単に panic
が呼ばれるだけのスタブになっており、さらに残念なことに最新版 v1.9 になってもやはり 実装はされない まま放置されています。
rkt app サブコマンドと rktlet
さて、rkt もここで Kubernetes 連携を諦めたわけではありません。放置されている rktshim
に代わって、新しいプロジェクト rktlet の開発が進められています。rktlet は Kubernetes Incubator プロジェクト の中の一つであり、中心となっているメンバは rkt のメンテナの件で言及した Kinvolk 社です。
rktlet では、以下の二つの変更によって CRI 対応を実現しようとしています。
まず、rkt そのものに手を入れることで、CRI に対応するために新たにPod に対して mutable な操作を行う rkt app
サブコマンドが追加されました。
さらに、デーモンを持たないという哲学は妥協して、rktlet がデーモンとして起動し gRPC を待ち受ける仕組みになりました。ただしコンテナ操作のロジックをデーモンに持たせることはせず、あくまでも rkt を外部コマンドとして呼びます。
実際に触って試してみましょう。とはいえ Kubelet から呼ばれている最中の様子を確認するのは原理的に難しいので、まず手で rkt app
コマンドを実行してみて、それから実際の Kubernetes との連携に進みます。
rkt app サブコマンド
rkt app
サブコマンドを使用することで、もともと rkt ではできなかった Pod の mutable な操作が可能になります。
最新版の v1.29.0 でもまだ開発中のステータスなので、コマンド実行時には環境変数 RKT_EXPERIMENT_APP=true
を指定しましょう。
まず空の Pod の作成です。CRI の RunPodSandbox
に相当する操作です。
$ sudo RKT_EXPERIMENT_APP=true rkt app sandbox (別ターミナルで) $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS c77b3f3b - - running 1 minute ago 1 minute ago default:ip4=172.16.28.2 $ ip netns list cni-a08f779f-ce1e-771b-b174-20ba662e6b7e (id: 0)
今までと異なり、APP
に何も登録されていない Pod が作られました。しかし NETWORK
欄や ip netns list
コマンドの結果を見ると、この段階ですでに namespace が先に作成されているのがわかります。
次に、この Pod の中にコンテナを追加してみます。CRI の CreateContainer
+ StartContainer
に相当します。
$ sudo RKT_EXPERIMENT_APP=true rkt app add c77b3f3b coreos.com/etcd:v3.1.7 $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS c77b3f3b etcd coreos.com/etcd:v3.1.7 running 7 minute ago 7 minute ago default:ip4=172.16.28.2 $ sudo RKT_EXPERIMENT_APP=true rkt app add c77b3f3b docker://redis:4.0.6 $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS c77b3f3b etcd coreos.com/etcd:v3.1.7 running 7 minute ago 7 minute ago default:ip4=172.16.28.2 redis registry-1.docker.io/library/redis:4.0.6
逆に特定のコンテナだけ終了させて Pod から外すことも可能です。CRI の StopContainer
+ RemoveContainer
に相当します。
$ sudo RKT_EXPERIMENT_APP=true rkt app rm c77b3f3b --app redis $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS c77b3f3b etcd coreos.com/etcd:v3.1.7 running 24 minutes ago 24 minutes ago default:ip4=172.16.28.2
表面上は全く同じように見えますが、rkt app
コマンドで作られた Pod は通常の rkt run
で作られた Pod とは別物です。例えば rkt run
で作られた Pod に後からコンテナを追加しようとするとエラーになります。
$ sudo rkt run coreos.com/etcd:v3.1.7 (別ターミナルで) $ sudo rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS 5c786d20 etcd coreos.com/etcd:v3.1.7 running 3 minutes ago 3 minutes ago default:ip4=172.16.28.2 $ sudo RKT_EXPERIMENT_APP=true rkt app add 5c786d20 docker://redis:4.0.6 add: error adding app to pod: immutable pod
Pod が mutable であるかどうかは rkt cat-manifest
を用いて Pod の低レベルな情報を取得することで判断できます。Docker で言えば docker inspect
に相当するコマンドです。
$ sudo rkt cat-manifest 5c786d20 ... "annotations": [ { "name": "coreos.com/rkt/stage1/mutable", "value": "false" } ], ...
rktlet のインストール
さて、kubelet から gRPC を待ち受けて rkt app
を実行するのが rktlet の役目です。
Unix ソケットで通信している都合上、各 Node 上で(Kubelet と同じサーバで)rktlet が稼働している必要があります。こちらもバイナリがリリースされているのでダウンロードしてくればすぐ使えます。
$ wget https://github.com/kubernetes-incubator/rktlet/releases/download/v0.1.0/rktlet-v0.1.0.tar.gz $ tar xzvf rktlet-v0.1.0.tar.gz $ sudo mv rktlet-v0.1.0/rktlet /usr/local/bin/rktlet $ rktlet --version rktlet version: v0.1.0
直接フォアグラウンドで実行したまま実験してももちろん構いませんが、もし systemd に登録するのであればテスト用の最低限のサービス定義は以下のようになります。各 Node の /etc/systemd/system/rktlet.service
に配置してください。
[Unit] Description=rktlet: The rkt implementation of a Kubernetes Container Runtime Documentation=https://github.com/kubernetes-incubator/rktlet/tree/master/docs [Service] ExecStart=/usr/local/bin/rktlet Restart=always StartLimitInterval=0 RestartSec=10 [Install] WantedBy=multi-user.target
登録後、サービスとして起動しておきます。
$ sudo systemctl enable rktlet $ sudo systemctl start rktlet $ systemctl status rktlet ● rktlet.service - rktlet: The rkt implementation of a Kubernetes Container Runtime Loaded: loaded (/etc/systemd/system/rktlet.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2017-12-22 17:52:06 UTC; 3s ago ...
Kubelet の設定
さて、次は呼び出す側の設定です。各 Node で稼働している Kubelet の実行時引数に、以下の四つの設定を入れてください。
--cgroup-driver=systemd --container-runtime=remote --container-runtime-endpoint=/var/run/rktlet.sock --image-service-endpoint=/var/run/rktlet.sock
--container-runtime-endpoint
CRI の RuntimeService に、--image-service-endpoint
が CRI の ImageService に、それぞれ対応していて、Unix ソケット経由で gRPC リクエストが送られるようになっています。
具体的に変更するファイルは Kubernetes をどうやって構築したかに依存するので何とも言えませんが、systemd サービスとして起動させているのであれば /etc/systemd/system/kubelet.service
内の ExecStart
もしくは EnvironmentFile
に記述があるはずです。
一旦 Kubelet を停止して、稼働している Docker コンテナをすべて止めておくのが安全です。その後、Kubelet を再起動して Node が Ready
になるまで待って確認してみると、Node のコンテナランタイム情報は狙い通り rkt に変わっています。
$ kubectl describe nodes/YOUR_NODE_NAME ... Container Runtime Version: rkt://0.1.0 ...
ただし rkt ではなく rktlet のバージョンが表示されてしまうようです。
実際に立ち上がっている rkt コンテナを確認してみましょう。rktlet はデフォルトの rkt とは別の場所、/var/lib/rktlet/data
以下にコンテナの情報を格納するようになっています。
クラスタ上に何がデプロイされているかによって細かい部分はもちろん違うと思いますが、おおむね以下のように Pod の一覧が表示されるはずです。
$ sudo rkt --dir=/var/lib/rktlet/data list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS 019e15b8 0-kubernetes-dashboard gcr.io/google_containers/kubernetes-dashboard-amd64:v1.6.3 running 3 minutes ago 3 minutes ago default:ip4=172.16.28.2 03183628 0-kube-proxy quay.io/coreos/hyperkube:v1.8.0_coreos.0 running 3 minutes ago 3 minutes ago 37bf1575 0-nginx-proxy registry-1.docker.io/library/nginx:1.11.4-alpine running 3 minutes ago 3 minutes ago 7996fe61 0-autoscaler gcr.io/google_containers/cluster-proportional-autoscaler-amd64:1.1.1 running 3 minutes ago 3 minutes ago default:ip4=172.16.28.5 abf84ed7 0-calico-node quay.io/calico/node:v2.5.0 running 3 minutes ago 3 minutes ago 1-calico-node quay.io/calico/node:v2.5.0 bf67a103 0-kubedns gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.2 running 3 minutes ago 3 minutes ago default:ip4=172.16.28.4 0-dnsmasq gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.2 0-sidecar gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.2
この後は通常通り、Deployment やその他のリソースを作成すればマニフェストの記述に従って rkt のコンテナが立ち上がるはずです。
まとめ
今回の記事では、CoreOS 社が作ったシンプル指向のコンテナ技術 rkt の仕組み、および rktlet を用いた Kubernetes との連携について実際のコマンドを交えつつ説明しました。
- rkt は Docker とは別の Pod ネイティブな仕組みを採用した
- しかしそれが裏目に出て、CRI への対応が難しくなった
- kubelet と rkt コマンドの間に rktlet を置き、新しく実装された
app
サブコマンドを叩かせることで CRI 相当の動作を実現する
ところで、今回の記事では rkt の内部アーキテクチャにはそれほど深く踏み込めませんでした。従来の immutable な Pod と新しい mutable な Pod、それぞれがどうやって systemd 上に実装されているか、というのも興味深い話題ではあるのですが、それはまた別の話。
以上、Kubernetes Advent Calendar 2017 の 23 日目の記事でした。明日はクリスマスイブですね。担当は tkusumi さんです。
NGK2017B で Liquid Haskell について話してきました
先日、毎年恒例のなごや LT 大会 NGK2017B / 名古屋合同懇親会 2017 忘年会 で発表してきました。
www.slideshare.net
当日の動画は NGK2017B 第2部 - YouTube から見ることができます。
ちなみに NGK での発表は 5 年連続 5 回目です。前回まではモデル検査をテーマにしていましたが、今回はちょっと目先を変えて(とはいえ同じ形式手法の枠内ですが)もう少しプログラム寄りの題材として Liquid Haskell を選びました。
- NGK2013B : Alloy ではじめる簡単モデル検査
- NGK2014B : 猫でもわかる! モデル検査器 SPIN 入門(ブログ記事)
- NGK2015B : AWS は形式手法の夢を見るか? - モデル検査器 Alloy によるインフラ設計(ブログ記事)
- NGK2016B : 机上の Kubernetes - 形式手法で見るコンテナオーケストレーション #NGK2016B(ブログ記事)
発表中のサンプルについて
今回の発表では、Heartbleed 脆弱性を模した次のようなコードをサンプルとして取り上げました。
module Main where import Data.Text import Data.Text.Unsafe {-@ measure tlen :: Text -> Int @-} {-@ assume pack :: s:String -> { t:Text | tlen t == len s } @-} {-@ assume takeWord16 :: n:Int -> { t:Text | tlen t >= n } -> Text @-} output :: Text output = takeWord16 10 (pack "input")
このアイデアは、 ICFP 2016 で行われた Niki Vazou さんのチュートリアルから拝借させていただいたものです。
Programming with Refinement Types
上のコードからもわかる通り、takeWord16
は Data.Text.Unsafe
モジュール内にあり、あくまでも unsafe な関数であることは明示されています。LT ではあえて「C 言語と同レベル」と煽ってみましたが、そもそも Haskell 本来の哲学からは外れた関数であることには留意してもらえると幸いです。
特に注目したいポイントは以下の 2 点です。
main 関数がない : エントリポイントがないため、プログラムは実行されておらずあくまでも静的な検証のみが行われていることがわかります。にもかかわらず、通常であれば実行時まで発見できないはずのロジックや値の範囲のエラーが検証できることこそが Liquid Haskell のメリットです。
篩型はコメントで記述する : 言語自体に特殊な追加要素を持ち込むことなく、コメントとして制約を記述します。通常の GHC から見ると{-@ ... @-}
内は単なるコメントでしかないため、Liquid Haskell を使用しない環境でもソースを変更することなくコンパイル可能です。
ちなみに、Liquid Haskell については去年の Advent Calendar でも解説しています。今回のスライドと合わせて読むと理解が深まるはずですが、それはまた別の話。
現在時刻をモックする Haskell ライブラリ time-machine を作ってみました
主としてテスト時のために、現在時刻を操作する Haskell ライブラリを作成しました。Hackage にも登録済みです。
試しに次のコードを実行してみましょう。getCurrentTime
しているはずなのに、返ってくる値が 1985 年 10 月 26 日になっているはずです。
module Main where import Control.Monad.TimeMachine import Control.Monad.Trans ( liftIO ) main :: IO () main = backTo (the future) $ do t <- getCurrentTime liftIO . putStrLn $ "We are at " ++ show t
作成の動機
一般論として、現在時刻に依存する関数やメソッドはテストが難しくなります。例えば次の関数を考えましょう。
getGreeting :: IO String getGreeting = do t <- getCurrentTime if utctDayTime t <= 12 * 60 * 60 then return "Good morning" else return "Hello"
この関数は午前中には "Good morning"
を、午後には "Hello"
を返しますが、時刻に依存して結果が変わってしまうため当然このままではテストできません。最初から時刻を引数として渡すようにするのも一つの方法ではありますが、今回はちょっと別の選択肢を考えます。
今回作成したライブラリ time-machine
を用いると、関数の中身はそのまま型だけ変更して
getGreeting :: (MonadTime m) => m String getGreeting = do t <- getCurrentTime if utctDayTime t <= 12 * 60 * 60 then return "Good morning" else return "Hello"
としておくことで、内部の getCurrentTime
が返す時刻を自由に操作できるようになります。もちろん、普通に IO
モナドのコンテクストでこの関数を呼んだ場合には普通に現在時刻を返して来るようになっています。
ちなみに、Ruby ではテスト時に現在時刻をモックするための gem として以下の 2 つがよく知られています。
今回のライブラリはこの gem から着想を得ています。モナドによる DSL を用いることで、同様の効果をより Haskell らしい方法で実現することを目指しました。
使い方
ライブラリ time-machine
が提供する主な関数は travelTo
、jumpTo
、accelerate
の 3 つです。使用することでモナドのコンテクストに入り、コンテクスト内で getCurrentTime
など現在時刻に依存する IO
アクションを使用するとモックされた値が返ってきます。
以下では具体的な使い方について説明します。なお各関数は独立した効果を持ちますが、モナドを入れ子にすることで複数の効果を同時に得ることも可能です。
travelTo
現在の(グローバルな)時刻を変化させます。タイムゾーンは変わりません。timecop の travel
に相当します。
main = travelTo (oct 26 1985 am 1 24) $ do getCurrentTime >>= (liftIO . print)
このコードでは、現在のタイムゾーンにおける 1985 年 10 月 26 日 AM 1:24 を指定しています。getCurrentTime
はタイムゾーンに関係なく UTC を返すので、実際には時差を補正した時刻が表示されることになります。
「行き先」となる時刻の指定にはいくつかの方法があります。
行き先を指定するための DSL は Control.Monad.TimeMachine.Cockpit
モジュールに定義されており、例えば travelTo (3 `days` ago)
のような自然言語っぽい記述ができるようになっています。ちなみにこの記事の冒頭で登場した backTo
も travelTo
のエイリアスです。
jumpTo
travelTo
とは逆に、現在時刻 (UTC) を保ったまま loadLocalTZ
の結果を変化させます。
import qualified Data.Time.Zones as TZ main = jumpTo "Asia/Shanghai" $ do t <- getCurrentTime tz <- loadLocalTZ liftIO . print $ TZ.timeZoneForUTCTime tz t -- CST
なお loadLocalTZ
だけではタイムゾーンが確定しないことに注意しましょう。これは、同じ地域でも UTC によってサマータイムになるかどうかが変わるためですが、time-machine
はサマータイムも含めて正しく扱えるようになっているはずです。
accelerate
時間が進む速さを変化させます。timecop の scale
に相当します。
main = accelerate (x 60) $ do getCurrentTime >>= (liftIO . print) -- (1) liftIO . threadDelay $ 1000 * 1000 getCurrentTime >>= (liftIO . print) -- (2)
このコードでは実時間 1 秒(1000 * 1000
マイクロ秒)のディレイが入っていますが、(2)
で表示される時刻は (1)
で表示される時刻の約 1 分後になります。これは accelerate (x 60)
の効果で、コンテクスト内部の時間が 60 倍に加速しているためです。
なお、accelerate
の特殊な場合として halt
が用意されています。コンテクスト内で時間のかかる処理を行っても時刻が変化しなくなるため、travelTo
を組み合わせて使用すると、テストしたい処理自身の実行にかかる時間を無視して狙った時刻をピンポイントに作り出すことができます。timecop の freeze
に相当する機能です。
main = halt $ do travelTo (jan 1 1970 am 0 0) $ do ....
仕組み
裏側の仕組みはシンプルで、型クラスを使うことでコンテクストによって挙動を変化させています。型クラス MonadTime
には、モナドのコンテクスト内部において時刻の情報を返すための関数が定義されています。
class (Monad m) => MonadTime m where getCurrentTime :: m T.UTCTime getCurrentTZ :: m TZ.TZ getCurrentTimeScale :: m TimeScale
実際にモックされた時刻のコンテクストを保持しているのはモナド変換子 TimeMachineT
で、実装には ReaderT
を流用しています。
getCurrentTZ
と getCurrentTimeScale
は一つのコンテクスト内では変化しないので実質単なる ask
をそのまま使っていますが、getCurrentTime
は「travelTo
したあとそのコンテクスト内で経過した時間」が必要なので別途算出しています。
instance (MonadIO m) => MonadTime (TimeMachineT m) where getCurrentTime = TimeMachineT $ do realCurr <- liftIO T.getCurrentTime Spacetime simOrigin realOrigin _ scale <- ask let diff = scaledDiffUTCTime scale realCurr realOrigin return $ T.addUTCTime diff simOrigin getCurrentTZ = TimeMachineT $ ask >>= return . stTZ getCurrentTimeScale = TimeMachineT $ ask >>= return . stTimeScale
travelTo
、jumpTo
、accelerate
の実体はこの TimeMachineT
を run
するための関数です。
IO
もまた MonadTime
のインスタンスになっており、かつ本物の getCurrentTime
や getCurrentTZ
が実装として指定されているため、IO
内で呼ばれた場合には真の現在時刻が返る、という仕組みになっています。
まとめ
今回作成したライブラリ time-machine
を使用すると、delorean
や timecop
の Ruby gem と同様、現在時刻をモックして時刻依存の関数の挙動を外から操作できるようになります。
内部では型クラスを用いて実装されており、コンテクストによって getCurrentTime
の挙動が変わることを利用しています。
なお、型クラスとモナド変換子を同様の考え方で用いることで、時刻に限らず一般に副作用をモックするライブラリとして monad-mock があります。こちらは Template Haskell を使っていたりしてもっと複雑ですが、それはまた別の話。
技術書典 3 で新刊落としました
先日、秋葉原で開催された技術系同人誌のオンリーイベント 技術書典 3 にサークル参加しました。
本当は新刊として Scala 用の静的解析ツール Stainless の入門書を頒布する予定で、サークルカットも完全にその線で準備していたのですが、残念ながら諸事情につき完成しませんでした。
事前にサークルチェックしてくださっていた 36 名の方々には大変申し訳ありませんでした。チェシャ猫先生の次回作にご期待ください。
何も並べるものがないのはちょっとどうかと思ったので、当日は 前回の技術書典 2 で頒布 した既刊『入門 LiquidHaskell』の PDF 版(ダウンロードカード)を持ち込みました。
頒布実績は終了時刻の 17 時まで粘って 31 部です。前回は 14 時頃に準備した 50 部が完売だったので、単純に計算すると 30% 程度のペースだったことになりますね。正直なところ、特殊な題材の割に、既刊にしては思ったより数が出たなという印象です。
なお、BOOTH での PDF 版の委託頒布は引き続き行っていますのでよろしくお願いします。
今回新しく運営サイドから決済アプリが提供されましたが、手元に Android 端末がない(iPhone 未対応)こともあり、また直前にアナウンスされたこともあって今回は対応しませんでした。お隣のサークルが割とアプリ決済されていたようだったので、次回は検討してみようと思います。
また、「立ち読みコーナー」が新設されました。あらかじめ運営に 1 部(見本誌とは別に)提出しておくと、メイン会場とは別に用意された部屋に並べてくれる仕組みです。上にも書いた通り今回は物理書籍の頒布予定がなかったので、キンコーズで PDF から中綴じ印刷して立ち読み用として提出しました。
さて、今回お蔵入りになった Stainless ネタはどうしましょうね? おそらく次回の技術書典 4 は来年 4 月頃に開催されるはずですが、それはまた別の話。
Serverless Meetup Tokyo #6 で Kubernetes について話してきました
先日行われた Serverless Meetup Tokyo #6 で、Kubernetes 上で動作する Serverless フレームワーク Fission について発表してきました。
www.slideshare.net
先週も 似たようなブログ を書いたような気がしなくもないですが、Kubeless と Fission を比較した前回の発表に対し、今回は話題を Fission に限定しています。一方で追加要素として、複数の Function を組み合わせるためのアドオン Fission Workflows を紹介してみました。
コンテナの再利用
スライド中でも言及している通り、一度「特殊化」されたコンテナは一定時間そのまま起動しており、再度同じ Function が呼び出された際に再利用されます。すなわち Function がコンテナ内部の環境に対して副作用を持つ場合、良くも悪くも次の呼び出しに影響するはずです。これを実際に確認してみましょう。
以下ではすでに 公式手順 に従って Fission が Kubernetes クラスタ上にデプロイされ、手元で fission
コマンドが使用可能になっているとします。
Environment の作成
スライド中で挙げられていたコマンド例は Python でしたが、単純にするためにここではシェルスクリプトを使います。
$ fission env create --name binary --image fission/binary-env:0.3.0 Use default environment v1 API interface environment 'binary' created $ fission env list NAME UID IMAGE binary 00386979-b3c2-11e7-b4db-fa163ed87208 fission/binary-env:0.3.0
Function と Route の作成
コンテナが再利用されていることを確かめるために、コンテナ内の状態を変化させるような恣意的な Function を作成します。
以下のコードを counter.sh
として保存します。なお公式で用意されている実行環境 fission/binary-env
では Bash は使用できず、shebang に sh を指定する必要があるので地味に気をつけましょう。
#!/bin/sh date >> /tmp/counter.txt cat /tmp/counter.txt | wc -l
これで呼び出しごとにコンテナ内のファイルに追記が行われるはずです。このソースコードを指定して Function を作成します。
$ fission function create --name counter --env binary --code counter.sh function 'counter' created $ fission function list NAME UID ENV counter 217af646-b3c2-11e7-b4db-fa163ed87208 binary
さらに、この Function を呼び出すためのトリガーを作成します。
$ fission route create --method PATCH --url /counter --function counter trigger '70a978a9-8cf9-42c3-a018-be0234785bc8' created $ fission route list NAME METHOD URL FUNCTION_NAME 70a978a9-8cf9-42c3-a018-be0234785bc8 PATCH /counter counter
これでエンドポイントが定義されました。fission route create --help
で出てくるオプション --method
の候補に PATCH
は入っていませんが、今回の例では期待した通りに動作します。
Function の実行
さて、この Function を実行してみると、
$ curl -X PATCH http://$FISSION_ROUTER/counter 1 $ curl -X PATCH http://$FISSION_ROUTER/counter 2 $ curl -X PATCH http://$FISSION_ROUTER/counter 3 $ curl -X PATCH http://$FISSION_ROUTER/counter 4
となり、実際に以前の実行の影響が残っていることがわかります。ちなみに 3 分ほど放置すると特殊化されたコンテナが消えるため、カウンタの値はリセットされます。
Kubernetes 側から直接確認してみましょう。プールされている 3 個 + 特殊化された 1 個で計 4 個の Pod があることが分かります。
kubectl -n fission-function get pods NAME READY STATUS RESTARTS AGE binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-189128r9t8 2/2 Running 0 6m binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912fhfp0 2/2 Running 0 2m binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912fpjtc 2/2 Running 0 8m binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl 2/2 Running 0 8m
ひとつだけ AGE
が若い Pod は、特殊化されて Deployment の管理下から抜けた Pod の穴を埋めるために新しく立ち上がったものです。今回の場合、Function counter
用に特殊化された Pod は 4 番目に表示されているもので、kubectl describe
コマンドで確認するとラベルがひとつだけ付け替えられています。
$ kubectl -n fission-function describe pods/binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl Name: binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl Namespace: fission-function Node: calico-1/192.168.235.231 Start Time: Wed, 18 Oct 2017 05:05:48 +0000 Labels: functionName=counter functionUid=217af646-b3c2-11e7-b4db-fa163ed87208 poolmgrInstanceId=T2bveCV8 unmanaged=true ...
実際にコンテナの中を覗くと、これまでの Function の呼び出しによって生成されたファイルが残っているはずです。
$ kubectl -n fission-function exec binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl cat /tmp/counter.txt Defaulting container name to binary. Use 'kubectl describe pod/binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl' to see all of the containers in this pod. Wed Oct 18 05:12:20 UTC 2017 Wed Oct 18 05:12:38 UTC 2017 Wed Oct 18 05:12:39 UTC 2017 Wed Oct 18 05:12:40 UTC 2017
まとめ
Kubernetes 上の Serverless フレームワーク Fission について、間をおかず同じ Function が呼ばれた場合にコンテナの再利用が行われることを簡単な実験で確認しました。
なお発表中ではスライド 1 枚で済ませてしまいましたが、複数の Function でピタゴラスイッチできる Fission Workflows はなかなか面白い仕組みです。いずれちょっとしたサンプルを公開したいと思っていますが、それはまた別の話。
Kubernetes Meetup Tokyo #7 で Serverless について話してきました
先日行われた Kubernetes Meetup Tokyo #7 で、Kubernetes 上で動作する Serverless フレームワークについて発表してきました。
www.slideshare.net
Serverless on Kubernetes を謳うツールはいくつかありますが、今回はそのうち Kubeless と Fission に焦点を当て、それぞれのアーキテクチャの違いを比較してみました。
当日の補足
Twitter 上で反応をもらった点についていくつか補足します。
Function の合成
うーん、これをピタゴラ装置に仕立て上げる部分が別に必要なのかどうかが知りたい #k8sjp
— Ken Ojiri (@kenojiri) 2017年10月12日
回答になっているかどうかちょっと自信がないのですが、複数の Function を連鎖させるという意味であれば、ごく最近 Fission Workflow というツールがリリースされています。
Fission の追加コンポーネントになっていて、Function を他の Function のトリガにするための仕組みです。有名どころで例えるなら AWS Step Functions が近いでしょうか。
クラウド FaaS との比較
Serverlessって1Functionだとやれることは限られる。なので、何かと何かを繋ぐグルーコードが多くなると思うんだけど、k8sでこれを動かすことでどういうユースケースがあるのかイマイチぴんとこないのよな #k8sjp
— Kazuto Kusama(草間 一人) (@jacopen) 2017年10月12日
正直なところ自分も同じような印象を持ちました。クラウドベンダ各社が提供している FaaS の強みはマネージドサービスとの連携であって、その恩恵に預かれない Kubernetes はだいぶ Serverless のメリットが目減りしている感じです。
現状でメリットと言えなくもない点は以下のふたつ。
まず当然ですが、なんらかの政治的な事情によりパブリッククラウドが使用できない場合でも使用可能な点。どうせなら Serverless フレームワークと互換性があれば環境間の差異が吸収できるのですが、今のところ Fission はこの機能は提供していないようです。ちなみに Kubeless については Serverless Framework Plugin が提供されています。
もうひとつは、Kubernetes の Events をトリガとして使用できる点です。例えば Slack に通知するとか、外部サービスとのちょっとした連携には役に立つかもしれません。現状トリガに指定できる条件は Namespace、Kind、Label のみですが、前述の Fission Workflow と組み合わせて何かちょっとしたサンプルが作れないかと考えているところです。
総論
現状、ものすごく画期的だとは言い難いですが、とはいえ数週間に一回アーキテクチャごと変更されていたり、Fission Workflow のような新しいツールが動き始めていたりして、決して停滞しているわけではなさそうです。まあ今後に期待しておきましょう。
なお Fission Workflow についてはもう少し調べた上で記事にまとめるか、あるいはまた別の場所で LT にでも仕立てようと狙っていますが、それはまた別の話。
Haskell ライブラリにスターを送るツール thank-you-stars を作ってみました
Haskell の GitHub レポジトリを眺めてみると、有名ライブラリであってもスター数が意外と少ないことがあります。かの Yesod ですら本記事執筆時点で 1,794 個であり、Rails の 36,933 個や Django の 28,165 個と比較すると文字通り桁違いです。
スター数は必ずしも OSS としての評価や価値を反映しませんし、そもそも Haskell ユーザの絶対数が少ないからと言ってしまえばそれまでなのですが、若干寂しい感じがしません?
一方、先日 id:teppeis さんが 依存しているライブラリにスターを送る npm ツール を公開されていました。そこで真似して作ってみた Haskell 版が以下です。
セットアップ後、自分の Cabal / Stack プロジェクトのルートディレクトリで実行するだけで OK。package.cabal
から依存ライブラリを読み取り、それが GitHub にホスティングされていればスターを付けにいってくれます。
なお、現在の実装では依存先ライブラリのレポジトリ情報をローカルの Hackage DB から取得しています。初めて package.cabal
に記述したライブラリはローカルに存在しない可能性があるので、一度ビルドを通してから試してください。スターを付ける GitHub API は冪等なので重ねて実行しても大丈夫です。
ちなみに、このツール自体が予想以上にスターを集めてしまい、