ねこのて

- nekonote -

haconiwa をお試ししてみた

f:id:dojineko:20170504174857j:plain

Yet another container, haconiwa.


そもそもコンテナってなんじゃ

「コンテナ」というと真っ先に「Docker」が思い浮かびますね。

軽いつかみとして用途を一つ上げるとすると、アプリの開発環境を用意するのに いちいちVM上げて、サービスが上がってくるの待つのがだるいから コンテナで環境を用意してその中でアプリを起動するみたいな感じに使います。

その実は、 Linux Kernel の機能を駆使して、実行環境を隔離する技術です。
主に下記のような技術が利用されています。
1つ1つを掘り下げると大変なことになるので「フワッ」っとご紹介します。

chroot

Change Root の名前の通り、指定したプロセスとその配下の子プロセスに対して、
仮想的にルートディレクトリを変更する機能です。

例えば bash を起動するときに chroot/var/www/virtual_home/
ルートディレクトリに見えるように指定すると、
起動した bash 上で cd / を実行したときに 実際にアクセスするのは
/var/www/virtual_home/ というようにすることができます。
言い換えると隔離された環境から /var/www/virtual_home/ より上の
ディレクトリは見えなくなります。

ルートディレクトリが変わるということは、chroot 後に
アプリを起動するためのライブラリなどの資産は全て chroot 後の
ディレクトリ配下に揃っている必要がある点に注意しましょう。

namespace

C++, C# などでも出てくる namespace と同じく、Linuxnamespace はプロセスの名前空間を定義します。
namespace を利用すると任意のプロセスを、大本の環境から仮想的に隔離した状態で実行することができます。

例えば bash を initプロセス(PID:1) として起動し子プロセスをぽこぽこ起動するということができます。
namespace が隔離できるのは PIDだけでなく、ユーザーID、グループID、マウントポイントなど他にもありオプションで指定可能です。

コンテナのメリットって?

複数VMを起動するというようなシーンでは、Linux Kernel の機能を使って仮想的に実行環境を隔離するため、 個々のVMで動くOS分のリソースを削減することができます。(よく見るあの図がそういう理解になります。)
その為、コンテナの起動と言うのはVMに比較して基本的に早くなります。

また namespace でプロセスが分離されているため別途 cgroup (リソース制限を行う仕組み)
などでコンテナに割り当てるCPUやメモリの制限を行うことができますし、
適切な運用がなされていれば chroot で専用のディレクトリ配下で動くので
大本のシステムに影響が出ることは少ないです。

コンテナのデメリットって?

Linux Kernel の機能を使っての仮想的な実行環境の隔離」といっている手前、
OSを上げる必要の無い分大本のKernel部分は各コンテナと共有されます。

その為特定のコンテナで Liunx Kernel に対して
特殊なパッチを当てる必要があるようなケースについては、
コンテナを実行する環境を別に用意する必要が出たりします。


「haconiwa」 とはなんぞ

id:udzura さんが主導開発されている mruby によるコンテナ実装です。
基本的には Docker と同じく chroot や namespace、cgroup を利用してコンテナを構築することができます。

Docker との違いは Dockerfile のような独自のシンプルなDSLではなく、
mRuby をつかってコンテナを記述することができる点です。

実装は mruby-cli を利用して行われており、chroot、namespace、cgroupを設定できる権限があれば
今のところは、Linux環境下で公式で配布されているバイナリを配置するだけで利用することができます。
(一部機能には LXC のインストールが必要ですが必須ではありません。今回はこの機能を使います)

https://github.com/haconiwa/haconiwa/releases から取得できるバイナリは3種類です。

  • haconiwa
    • haconiwa本体 Cli実装
  • hacoirb
    • haconiwaが組み込まれたirbコマンド相当の実装
  • hacorb
    • haconiwaが組み込まれたrubyコマンド相当の実装

基本的には haconiwa を入り口として触っていくことになります。

「haconiwa」 でコンテナを作ってみよう

コマンドラインについて

ひとまずコマンドラインのオプションを見てみましょう。こんな感じです。

# haconiwa -h
haconiwa - The MRuby on Container
commands:
    new       - generate haconiwa's config DSL file template
    create    - create the container rootfs
    provision - provision already booted container rootfs
    archive   - create, provision, then archive rootfs to image
    start     - run the container
    attach    - attach to existing container
    reload    - reload running container parameters, following its current config
    kill      - kill the running container
    version   - show version
    revisions - show mgem/mruby revisions which haconiwa bin uses

Invoke `haconiwa COMMAND -h' for details.

new やら create やら色々オプションがありますね。
コンテナの設定を作るには new を指定するようです。

# haconiwa new -h
 -n, --name=CONTAINER_NAME Specify the container name if you want
 -r, --root=ROOTFS_LOC     Specify the rootfs location to generate on host
 -G, --global              Create global config /etc/haconiwa.conf.rb
 -h, --help                Show help
 HACO_FILE                 Put the config file at the end of command

コンテナの設定ファイルを作る

物は試しに叩いてみましょう

# haconiwa new test001.haco
assign  new haconiwa name = haconiwa-44bb30b9
assign  rootfs location = /var/lib/haconiwa/44bb30b9
create  test001.haco

haconiwa-44bb30b9 という名前の、/var/lib/haconiwa/44bb30b9 rootfs にするような
コンテナの設定ファイルが出来上がったみたいです。
せっかくなので中身を見てみましょう。(コメントは省略)

$ cat test001.haco
Haconiwa.define do |config|
  # コンテナ名 (uname -n とかで出てくる名前)
  config.name = "haconiwa-44bb30b9"
  # Initプロセスの指定
  config.init_command = "/bin/bash"
  # rootfs の指定 (chroot する先)
  root = Pathname.new("/var/lib/haconiwa/44bb30b9")
  config.chroot_to root
  # コンテナ構築の起点の指定
  config.bootstrap do |b|
    b.strategy = "lxc"    # LXC-Create を使って
    b.os_type  = "alpine" # Alpine Linux のコンテナを作成する
  end
  # プロビジョニングの設定 (apk を使って bash をインストール)
  config.provision do |p|
    p.run_shell <<-SHELL
apk add --update bash
    SHELL
  end
  # その他マウントの設定やnamespaceの設定など (今回は省略)
  config.add_mount_point "tmpfs", to: root.join("tmp"), fs: "tmpfs"
  config.mount_network_etc(root, host_root: "/etc")
  config.mount_independent "procfs"
  config.mount_independent "sysfs"
  config.mount_independent "devtmpfs"
  config.mount_independent "devpts"
  config.mount_independent "shm"
  config.namespace.unshare "mount"
  config.namespace.unshare "ipc"
  config.namespace.unshare "uts"
  config.namespace.unshare "pid"
end

お、なんだか Vagrantfile に雰囲気が似てますね。
読めばなんとなくわかりそうな感じがします。

補足するとデフォルトではコンテナの構築時に lxc-create という
Linux Containers ( Dockerとは別のコンテナ技術 ) の機能を使い Alpine Linux の rootfs を作ることができます。
b.strategy = "lxc" という指定がそれですね。OSの指定は b.os_type = "alpine" という指定です。

もうちょっとだけ掘り下げると Linux Containers には
もともと他のOSのコンテナを作るためのテンプレートがあり
lxc-create コマンドに使いたいテンプレートを渡すとそれに準じて
コンテナ用のrootfsを構築してくれるというわけです。

# ls /usr/share/lxc/templates
lxc-alpine     lxc-busybox  lxc-debian    lxc-gentoo        lxc-oracle     lxc-sparclinux  lxc-ubuntu-cloud
lxc-altlinux   lxc-centos   lxc-download  lxc-openmandriva  lxc-plamo      lxc-sshd
lxc-archlinux  lxc-cirros   lxc-fedora    lxc-opensuse      lxc-slackware  lxc-ubuntu

デフォルトだとこれだけあります。
haconiwaに対し b.strategy = "lxc"b.os_type = "alpine" を指定すると
lxc-create に lxc-alpine を使うように指定して rootfs を作る というように読み替えることができます。

他にも strategy に指定できるものがあり debootstrap を指定すると
debian公式の機能を使ってコンテナを作れるほか、
gittarball を指定すれば gitリポジトリ や tar を展開して
rootfs としたり、shellmruby を指定すれば任意の処理に移譲して
rootfs を作ることができるようです。

ちなみに haconiwa そのものには Docker のような
レイヤー構造を持ったイメージという概念は今のところ存在しません。

そういうところについては別途ファイルシステムなどに移譲しているようで、
あくまでシンプルなコンテナの実装なんだという認識でいると良さそうです。

そのため Data Volume Container を用意してマウントしてあげないとデータを永続化できない
というようなことは基本的にはなく、Vagrant と Docker のいいとこ取りができてそうに感じました。

コンテナを構築して実行する

というわけでまずコンテナを構築してみましょう。
haconiwa new で作成したファイルを指定して haconiwa create を実行してあげます。

# haconiwa create test001.haco
Creating rootfs of haconiwa-44bb30b9...
Start bootstrapping rootfs with lxc-create...
[bootstrap.lxc]:    Obtaining an exclusive lock... done
[bootstrap.lxc]:
[bootstrap.lxc]:    ==> Fetching and/or verifying APK keys

# (中略) lxc-create を使って /var/lib/haconiwa/44bb30b9 に必要なファイルを作っていく

Command success: /bin/sh -xe exited 0
Success!

無事できたようですね。
早速起動してみましょう。 haconiwa start で起動することができます。
今回は init_command に bash が指定されているので起動すると
コンテナ上で動作する bash にターミナルが移ります。

# haconiwa start test001.haco
Container fork success and going to wait: pid=2742

# コンテナ名が出てくる
bash-4.3# uname -n
haconiwa-44bb30b9

# bash が PID 1 で起動し initプロセスになっていることがわかる
bash-4.3# ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
   10 root       0:00 ps

良さそうですね。実際にRailsアプリを動かしたりするシーンでは
nginx を initプロセスに指定して起動するか、あるいは supervisord を起動して、
さらに配下に必要なプロセスを起動させることになるとおもいます。

ちなみに 起動したコンテナは Ctrl+D で終了することで抜けることができます。

「haconiwa」のフック機能

haconiwa独特の機能として、mrubyによるフックの実装と言うものがあります。
より具体的に言及すると、

  • 「コンテナを起動する前にネットワークの設定をいじりたい」
  • 「コンテナを終了した後にメールを飛ばしたい」
  • 「コンテナの実行中に1分ごとに処理を実行したい」
  • 「特定のシグナルを受けたときに処理を実行したい」

というような機能を実装することができます。
これも試しに使ってみましょう。

作成されている test001.haco の最後に下記のようなブロックを定義します。
起動後3秒待って 1秒おきに画面に出力するようにしてみます

cnt = 0
config.add_async_hook(sec: 3, interval_msec: 1 * 1000) do |base|
  cnt = cnt + 1
  p("async hook called #{cnt} times [#{base.pid}]")
end

では早速起動してみましょう

# haconiwa start test001.haco
Container fork success and going to wait: pid=2791
bash-4.3# Async hook starting...
"async hook called 1 times [2791]"
Async hook starting...
"async hook called 2 times [2791]"
Async hook starting...
"async hook called 3 times [2791]"
Async hook starting...
"async hook called 4 times [2791]"
Async hook starting...
"async hook called 5 times [2791]"
Async hook starting...
"async hook called 6 times [2791]"
exit
Container(2791) finish detected: #<Process::Status: pid=2791,exited(0)>
Container successfully exited: #<Process::Status: pid=2791,exited(0)>
One of supervisors finished: 2790, #<Process::Status: pid=2790,exited(0)>

うまくいきましたね。
このように hacoファイルで好きなフックを書いてコンテナの動きをどんどん拡張していくことができます。
他にどんなフックが有るかは 公式のREADME 参考にすると良さそうです。

まとめ

今回はコンテナがどういう風に実現されているかをおさらいしたほか、
注目の haconiwa についてお試ししてみました。

使い勝手としては良いなーという感想を持ったとこで、
デフォルトだとデータの永続化を考えなくても使えるあたり
普段使いだとチョッパヤなVagrantの代替え的な扱いがし易いのではないかと思いました。
(Linux環境以外だと Docker for Windows/Mac 的な何かが別途必要ですが。)

※ またこの記事、触り始めたばかりなので間違ってたらごめんなさい (気がついたら修正します)

ブックマーク

github.com www.pakutaso.com

Golint < Json should be JSON!

f:id:dojineko:20170216213454j:plain

「えっ、そんなん言われても・・・」


使ってますか Golint

github.com

この子です。Golangを使ってるときには欠かせない(はず)の静的解析ツールです。

細かな関数名のスペルだったり、明らかに冗長なコードを書いていたり、
あるいは公開している関数や構造体なのにドキュメントコメントが無い場合などなど
色々アドバイスをくれる憎いやつです。

具体的にどんなことしてくれるのかは下記のスライドが詳しかったので紹介します。

www.slideshare.net

Golint の簡単な使い方

導入は go get から簡単に行なえます。

go get -u github.com/golang/lint/golint

usage がそのままなので抜粋します。
基本形は golint hoge.gogolint package-name です。

$ golint -h
Usage of golint:
    golint [flags] # runs on package in current directory
    golint [flags] [packages]
    golint [flags] [directories] # where a '/...' suffix includes all sub-directories
    golint [flags] [files] # all must belong to a single package
Flags:
  -min_confidence float
        minimum confidence of a problem to print it (default 0.8)
  -set_exit_status
        set exit status to 1 if any issues are found

ガツンとカレントディレクトリ配下全てのチェックを行う場合は下記のようにします。

# カレントディレクトリ配下のチェックを行う
golint ./...

Json should be JSON

ここで1つされやすい(と思われる)警告を例に上げてみます。

一般的な略語(URLとかXSSとかSSHとか) については合致するものは例えば、
Url や XssSsh と言うように記述しているときに警告されます。

その時は

func hogeFugaJson should be hogeFugaJSON

というように警告されます。

定義されている略語は下記のようになっています。

lint/lint.go at b8599f7d71e7fead76b25aeb919c0e2558672f4a · golang/lint · GitHub

とはいえチェックが五月蠅すぎる・・・

という場合はあると思うので、あくまで最終的な手段として
min_confidence というパラメータを設定することができます。

これは golint を実行した際に警告される最低レベルを実数で設定するとができます。
具体的には下記のような感じです。

golint ./... -min_confidence=1

デフォルトでは Usageにもあったように 0.8 で動作しています。

各解析結果を警告するかどうかはそれぞれで動作する段階があり、
例えば先のJson/JSON問題で言えば下記の行で指定がされており
0.8 で動作するようになっていることがわかります。

lint/lint.go at b8599f7d71e7fead76b25aeb919c0e2558672f4a · golang/lint · GitHub

つまり、この場合は0.9以上の値にすればこの警告については黙ってくれるということになるわけですね。

とはいえ難しいものでなければ可能な限りデフォルトで実行されるgolintに対応できると気持ち良いのではないかと思います

ブックマーク

www.pakutaso.com

io.TeeReader を使って Teeコマンドを作る

f:id:dojineko:20170211215431j:plain

下記のエントリに触発されたのでかるーくやってみました。
ターゲットは tee コマンドです。

takatoshiono.hatenablog.com

teeコマンドって?

標準入力で受け取ったデータを標準出力しながら、
指定したファイルにも出力するようなコマンドです。

Linuxコマンド集 - 【 tee 】 標準入力を標準出力とファイルに出力する:ITpro

他のコマンドの実行結果を tee にパイプして表示しつつログを取るというような使い方をしたりします。

io.TeeReaderって?

teeコマンドのように golang上で指定したReaderで読み取った内容を、
指定したWriterに渡しながら処理をすることのできるReaderです。

io - The Go Programming Language

サンプルプログラム

実際に使ってみないとなんともイメージが湧きにくいと思いますので
最低限 teeコマンドと同じ挙動をする gogotee というコマンドを作ってみます。

続きを読む

Let's EncryptでSSL証明書を取得してHTTP/2な世界にGopherくんを召喚してみた

f:id:dojineko:20170102154432j:plain

独自ドメインを持ってるなら、オレオレ証明書なんか使わずに
VALIDなHTTPSでヤッテイコーという試み。

続きを読む

GOPATHの運用を見なおしてみたら意外と便利だった

f:id:dojineko:20160909000527j:plain

go-get と ghq (+ fzf/peco) の連携 はとても便利

続きを読む

Windows で Vagrant となかよくする方法を考えてみた

f:id:dojineko:20160827232816j:plain

VirtualBoxVagrant を使ってファイル共有すると vboxfs がとにかく遅い。 macOS であれば NFS サーバーを立てて共有フォルダを設定すれば、だいぶ軽くなるけども Windows だと手軽には NFS サーバーを用意できないみたい。うーんどうすればいいものか。

続きを読む

Bashでちょっと凝ったオプションの解析をする

f:id:dojineko:20160630222829j:plain

痒いところに手が届かないのをなんとかしたかったのねん


Bashスクリプトを書いてたら、ちょっと凝ったオプション解析をしたくなった。

  • ロングオプションも使えるようにしたい
  • オプションの第二引数は任意にしたい

Bashスクリプトでオプション解析といえば getopt や getopts を使うと思いますが、

  • getopt はBashのビルトインコマンドなのでどこでも使えるけどロングオプションが使えない
  • getopts は BSD系(macOSなど) と GNU系(Linuxなど) で動きが違う

など微妙に痒いところに手が届かないので、下記のQiita記事を参考に、メモがてらちょっとアレンジしつつオレオレオプション解析を書いてみました。 qiita.com

続きを読む