ようへいの日々精進XP

よかろうもん

ゴールデンウィークスペシャル:Elixir チュートリアル (1) 〜パターンマッチ編〜

以前から

気になっていた Elixir という言語について、以下の資料を写経してチュートリアルしてみた。

www.slideshare.net

その前に Elixir (エリクサー) とは

そこには Erlang が…

  • Erlang の関数を呼び出せるため、Erlang のシステムにシームレスに統合出来る
  • Erlang 良い部分(並行性や信頼、耐障害性)をそのまま利用出来る(当然 Erlang の悪い部分も受け継いでしまっているらしい)

特徴

その他色々… こちらが参考になる。

エコシステム

  • ライブラリ管理に hex を利用する… Ruby だと gem
  • ビルドツールに mix を利用する… Ruby だと rake
  • 対話型シェルに iex を利用する… Ruby だと irb
  • Web Application Framework として Phoenix がメジャー… Ruby だと Rails

で、なんであんたは Elixir をやろうとしたの?

  • Ruby と似た文法ということで、以前から気になっていた
  • ギョームでたまに Erlang というワードが耳に入ってくる

チュートリアル

チュートリアル環境

  • OS は Ubuntu Xenial でお届けします
ubuntu@ubuntu-xenial:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
  • Elixir は 1.4.1 でお届けします
ubuntu@ubuntu-xenial:~$ elixir --version
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Elixir 1.4.1

Elixir のインストール

以下を実行して Erlang と Elixir をインストールする。

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
sudo apt-get update
sudo apt-get install esl-erlang
sudo apt-get install elixir

以下を実行して iex を起動することを確認する。

ubuntu@ubuntu-xenial:~$ iex
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)

対話シェルで操作してみる

対話シェル iex を起動して加算や list や map の操作を行ってみる。

iex(1)> 1+2
3
iex(2)> list = [ 123, "abc", 456, true ]
[123, "abc", 456, true]
iex(3)> Enum.sort( list )
[123, 456, true, "abc"]
iex(4)> map = %{ "key2" => "abc", "key1" => 123 }
%{"key1" => 123, "key2" => "abc"}
iex(5)> Enum.sort( map )
[{"key1", 123}, {"key2", "abc"}]
iex(6)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution

list や map の操作に Enum モジュールを利用している。Enum モジュール には eachempty? 等の関数が用意されていて、さながら Ruby と見紛う。

関数を定義する

新規プロジェクトの作成

新規プロジェクトを以下のように作成する。

mix new sample

以下のように出力される。

* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/sample.ex
* creating test
* creating test/test_helper.exs
* creating test/sample_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd sample
    mix test

Run "mix help" for more commands.

関数の作成

lib/sample.ex を以下のように記述する。

defmodule Sample do
  def sort( values ) do
    Enum.sort( values )
  end
end

関数のビルドと実行

記述したら、以下のコマンドを実行してビルドを行う。

cd sample
iex -S mix

iex が起動するので、以下のように作成した Sample 関数を実行する。

ubuntu@ubuntu-xenial:/vagrant/elixir/sample$ iex -S mix
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Sample.sort( [ 5, 1, 3 ])
[1, 3, 5]
iex(2)> Sample.sort [ 5, 1, 3 ] #=> () は省略可能
[1, 3, 5]

おお。

関数の修正と再ビルド

関数は end を省略して 1 行でも記載することが出来る。

defmodule Sample do
  def sort( values ), do: Enum.sort( values )
end

関数を修正したら recompile() を実行して再ビルドする。

iex(3)> recompile()
Compiling 1 file (.ex)
:ok

改めて Sample 関数を実行する。

iex(4)> Sample.sort [ 5, 1, 3 ]
[1, 3, 5]
iex(5)> Sample.sort %{ "key2" => "abc", "key1" => 123 }
[{"key1", 123}, {"key2", "abc"}]

map もキー名でソートすることが出来る。

パターンマッチ

引数でのマッチ

Sample 関数に以下を追加する。

def match( %{ Yes: need } ), do: need

追加したら recompile() する。

iex(7)> recompile()
Compiling 1 file (.ex)
:ok

以下のように Sample 関数を実行する。

iex(8)> Sample.match %{ No: "-", Yes: "we can", NA: "N/A" }
"we can"

マップの Yes キーの値を関数の引数だけで実装出来る。ちなみに、Ruby でやろうとすると、以下のような書き方になるんだろうか。

irb(main):001:0> def match(args = {})
irb(main):002:1> p args[:Yes]
irb(main):003:1> end
=> :match
irb(main):004:0> match({ No: "-", Yes: "we can", NA: "N/A" })
"we can"
=> "we can"

あー、改めて、関数の引数だけで値が取り出せるのは便利かも。

複数関数の呼び分け

値でもマッチさせることも可能なので、以下を追加して recompile() する。

def match( %{ Yes: "we can" } ), do: "Barack Obama" # 追加する
def match( %{ Yes: need } ), do: need

Sample 関数を実行してみる。

iex(2)> Sample.match %{ No: "-", Yes: "we can", NA: "N/A" }
"Barack Obama"

ほー。マップのキーをチェックして処理を分岐することが出来ている。これも Ruby でやろうとすると以下のような書き方になるんだろうか。

irb(main):001:0> def match(args = {})
irb(main):002:1> if args[:Yes] == "we can"
irb(main):003:2> p "Barack Obama"
irb(main):004:2> end
irb(main):005:1> end
=> :match
irb(main):006:0> match({ No: "-", Yes: "we can", NA: "N/A" })
"Barack Obama"
=> "Barack Obama"

マッチしない場合

キーが存在しない場合には、以下のようなエラーとなる。

iex(4)> Sample.match %{ No: "-", NA: "N/A" }
** (FunctionClauseError) no function clause matching in Sample.match/1
    (sample) lib/sample.ex:24: Sample.match(%{NA: "N/A", No: "-"})

以下のように関数の引数に _ を指定すると、その他としてマッチ出来るようになる。

def match( %{ Yes: "we can" } ), do: "Barack Obama"
def match( %{ Yes: need } ), do: need
def match(_), do: "Yes...Not EXIST" # 追加する

recompile() して関数を再実行する。

iex(5)> recompile()
Compiling 1 file (.ex)
:ok
iex(6)> Sample.match %{ No: "-", NA: "N/A" }
"Yes...Not EXIST"

例の如く、Ruby で書くと以下のようになるんだろうか。

irb(main):010:0> def match(args = {})
irb(main):011:1> p "Yes...Not EXIST" unless args.has_key?(:Yes)
irb(main):012:1> end
=> :match
irb(main):013:0> match({ No: "-", NA: "N/A" })
"Yes...Not EXIST"
=> "Yes...Not EXIST"

関数内でマッチ

関数の引数では無く、関数内でマッチさせる場合には以下のように書く。

defmodule Sample do
  def match_inner( input_map ) do
    %{ Yes: need } = input_map
    need
  end
end

以下のように引数マッチと同じ結果となる。

iex(10)> Sample.match_inner %{ No: "-", Yes: "we can", NA: "N/A" }
"we can"

引数マッチ版の方がシンプルな気がする。

リストのパターンマッチ

リストは先頭を head で、以降を tail でパターンマッチすることが出来る。

iex(1)> [ head | tail ] = [ 5, 8, 3, 1 ]
[5, 8, 3, 1]
iex(2)> head
5
iex(3)> tail
[8, 3, 1]

わかり易い言葉で書くと head はリストの最初の要素を取り出すことが出来て、tail はその残りを取り出すことが出来る。

以下のようにも書くことが出来るようだ。

iex(10)> list = [1,2,3]
[1, 2, 3]
iex(11)> hd(list)
1
iex(12)> tl(list)
[2, 3]

Ruby だと以下のように書くんだろうか。

irb(main):014:0> list = [ 5, 8, 3, 1 ]
=> [5, 8, 3, 1]
irb(main):015:0> list.first
=> 5
irb(main):016:0> list[-3..-1]
=> [8, 3, 1]

個人的には Elixir も Ruby も直感的な気がする。各コード量的には微々たるものだけど Elixir の方が少ないかもしれない。

リストのパターンマッチ(2)

headtail を利用することで、リストの巡回(再帰呼び出し)が可能となる。

defmodule Sample do
  def nums( [ head | tail ], rows ) do #...1
    nums( tail, [ head | rows ] )      #...2
  end
  def nums( [], rows ) do              #...3
    rows                               #...4
  end
end

関数を実行すると以下のように出力される。

iex(6)> Sample.nums( [ 5, 8, 3, 1 ], [] )
[1, 3, 8, 5]

少し理解に苦しんだので、以下のように整理した。

  1. リストの先頭を head で、残りを tail でマッチさせる
  2. tail に入っている残りの値の先頭を head でマッチさせる
  3. 2 の tail が空になったら 3. が呼ばれで 4. の rows が出力される

正直言って、まだボヤーっとしているので、headtail はもう少し弄る。

ちなみに、Ruby だと以下のように書くのかな…

irb(main):006:0> list = [5, 8, 3, 1]
=> [5, 8, 3, 1]
irb(main):007:0> new_list = []
=> []
irb(main):008:0> list.each { |l| new_list << l }
=> [5, 8, 3, 1]
irb(main):009:0> new_list.reverse
=> [1, 3, 8, 5]

リストのパターンマッチ(2) + 1

もすこし再帰呼び出しをやりたいので、list に格納されている数値の合計を求めてみる。

defmodule Sample do
  def sum_list( [ head | tail ], total ) do #...1
    sum_list( tail, head + total )          #...2
  end
  def sum_list( [], total ) do              #...3
    total                                   #...4
  end
end

おお、なんか理解が出来てきたけど整理。

  1. リストの先頭を head で、残りを tail でマッチさせる(初期値は 0)
  2. 1 の tailheadtotal に格納されている数値を加算(tail が空になるまで繰り返す)
  3. 2 の tail が空になったら 3. が呼ばれて 4. の total が出力される

Ruby だと inject メソッドを使って以下のように書くはず。

irb(main):010:0> list = [5, 8, 3, 1]
irb(main):011:0> list.inject(0) { | sum, l | sum + l }
=> 17

おお、シンプル。

ということで

sample.ex

今回のチュートリアルで作成した sample.ex は以下のようになった。

defmodule Sample do
  # def sort( values ) do
  #   Enum.sort( values )
  # end
  def sort( values ), do: Enum.sort( values )
  def match( %{ Yes: "we can" } ), do: "Barack Obama"
  def match( %{ Yes: need } ), do: need
  def match(_), do: "Yes...Not EXIST"
  def match_inner( input_map ) do
    %{ Yes: need } = input_map
    need
  end
  # def nums( [ head | tail ], rows ), do: nums( tail, [ head | rows ] )
  # def nums( [], rows ), do: rows
  def nums( [ head | tail ], rows ) do #...1
    nums( tail, [ head | rows ] )      #...2
  end
  def nums( [], rows ) do              #...3
    rows                               #...4
  end

  def sum_list( [ head | tail ], total ) do
    sum_list( tail, head + total )
  end
  def sum_list( [], total ) do
    total
  end
end

Elixir とは (再掲)

引き続き…

参考にさせて頂いた資料はまだまだ続くので、引き続き写経を続けていくぞー。

参考

2017 年 05 月 04 日(木)

ジョギング

日課

  • (腕立て x 30 + 腹筋 x 30) x 3

読書

Ruby スクリプティングテクニック を読み進めている。訳が少し複雑な感じで読みづらい部分があるが、少し修正すれば使えそうなツールを題材にしていたり、Test::Unit の話しが出てきたりして面白くなってきた。

ゴールデンウィークスペシャル:小ネタ道場一本勝負 〜 川原洋平探検隊が Elasticsearch の奥地に住まう猫に遭遇する! 〜

この記事は

川口浩探検隊へのオマージュです。

www.amazon.co.jp

cat API と何か!?(効果音:ジャーン)

www.elastic.co

cat API は Elasticsaerch のノードやインデックスの各種情報を取得する事が出来る便利 API で、Elasticsearch のトラブルシューティングやリソースの検討等に有用である!(効果音:ジャーン)ちなみに、個人的には以下のように各インデックスのシャード数やドキュメント数を取得出来るが嬉しい!(効果音:ジャーン)

ubuntu@ubuntu-xenial:~$ curl localhost:9200/_cat/indices?v
health status index   uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   twitter VSZ8zCTBS86Awm6n3pmckw   5   1          2            0      9.1kb          9.1kb

果たして Elasticsearch に猫が住んでいるのだろうか…(効果音:ジャーン)

尚、cat API の詳細についてはこちらのブログで紹介されている!(効果音:ジャーン)

本当に猫が住んでいるのか!?(効果音:ジャーン)

試した環境

ubuntu@ubuntu-xenial:~$ curl localhost:9200/
{
  "name" : "u0y-s7V",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "p20emC22SyG96ziu0qwl5w",
  "version" : {
    "number" : "5.3.2",
    "build_hash" : "3068195",
    "build_date" : "2017-04-24T16:15:59.481Z",
    "build_snapshot" : false,
    "lucene_version" : "6.4.2"
  },
  "tagline" : "You Know, for Search"
}

にゃー!

川原洋平探検隊は遂に見た!(効果音:ジャーン)

ubuntu@ubuntu-xenial:~$ curl localhost:9200/_cat
=^.^=
/_cat/allocation
/_cat/shards
/_cat/shards/{index}
/_cat/master
/_cat/nodes
/_cat/tasks
/_cat/indices
/_cat/indices/{index}
/_cat/segments
/_cat/segments/{index}
/_cat/count
/_cat/count/{index}
/_cat/recovery
/_cat/recovery/{index}
/_cat/health
/_cat/pending_tasks
/_cat/aliases
/_cat/aliases/{alias}
/_cat/thread_pool
/_cat/thread_pool/{thread_pools}
/_cat/plugins
/_cat/fielddata
/_cat/fielddata/{fields}
/_cat/nodeattrs
/_cat/repositories
/_cat/snapshots/{repository}
/_cat/templates

ん…

f:id:inokara:20170503093903p:plain

cat だけに…猫が住んでいたのであった!!(効果音:ジャーン)

そして、とうとう、川原洋平探検隊は…

ソースコードの中に猫を発見したのであった!(効果音:ジャーン)

github.com

f:id:inokara:20170503130114p:plain

以上

川原洋平探検隊が Elasticsearch の奥地に住んでいる猫を発見したメモでした。

小ネタ道場一本勝負 〜 Ubuntu xenial で Elasticsearch が Cannot allocate memory で起動しない場合 〜

試した環境

ubuntu@ubuntu-xenial:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"

ubuntu@ubuntu-xenial:~$ free
              total        used        free      shared  buff/cache   available
Mem:        1015992       49948      397164        3440      568880      770376
Swap:             0           0           0

ubuntu@ubuntu-xenial:~$ curl localhost:9200/
{
  "name" : "u0y-s7V",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "p20emC22SyG96ziu0qwl5w",
  "version" : {
    "number" : "5.3.2",
    "build_hash" : "3068195",
    "build_date" : "2017-04-24T16:15:59.481Z",
    "build_snapshot" : false,
    "lucene_version" : "6.4.2"
  },
  "tagline" : "You Know, for Search"
}

インストール直後

deb パッケージ をダウンロードしてインストール後に確認してみると…

ubuntu@ubuntu-xenial:~$ sudo service elasticsearch status
● elasticsearch.service - Elasticsearch
   Loaded: loaded (/usr/lib/systemd/system/elasticsearch.service; disabled; vendor preset: enabled)
   Active: failed (Result: exit-code) since Tue 2017-05-02 23:48:36 UTC; 4s ago
     Docs: http://www.elastic.co
  Process: 10100 ExecStart=/usr/share/elasticsearch/bin/elasticsearch -p ${PID_DIR}/elasticsearch.pid --quiet -Edefault.path.logs=${LOG_DIR} -Edefault.path.data=${DATA_DIR} -Edefault.path.conf=${CONF_DIR} (code=exited, status=1/FAILURE)
  Process: 10097 ExecStartPre=/usr/share/elasticsearch/bin/elasticsearch-systemd-pre-exec (code=exited, status=0/SUCCESS)
 Main PID: 10100 (code=exited, status=1/FAILURE)

May 02 23:48:36 ubuntu-xenial systemd[1]: Started Elasticsearch.
May 02 23:48:36 ubuntu-xenial elasticsearch[10100]: OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x000000008a660000, 1973026816, 0) failed; error='Cannot allocate memory' (errno=12)
May 02 23:48:36 ubuntu-xenial elasticsearch[10100]: #
May 02 23:48:36 ubuntu-xenial elasticsearch[10100]: # There is insufficient memory for the Java Runtime Environment to continue.
May 02 23:48:36 ubuntu-xenial elasticsearch[10100]: # Native memory allocation (mmap) failed to map 1973026816 bytes for committing reserved memory.
May 02 23:48:36 ubuntu-xenial elasticsearch[10100]: # An error report file with more information is saved as:
May 02 23:48:36 ubuntu-xenial elasticsearch[10100]: # /tmp/hs_err_pid10100.log
May 02 23:48:36 ubuntu-xenial systemd[1]: elasticsearch.service: Main process exited, code=exited, status=1/FAILURE
May 02 23:48:36 ubuntu-xenial systemd[1]: elasticsearch.service: Unit entered failed state.
May 02 23:48:36 ubuntu-xenial systemd[1]: elasticsearch.service: Failed with result 'exit-code'.

ふむ。

ということで、技あり

/etc/elasticsearch/jvm.options で JVM オプションを修正してヒープサイズを修正する。

root@ubuntu-xenial:/etc/elasticsearch# diff -u jvm.options.bk jvm.options
--- jvm.options.bk      2017-05-02 23:51:22.295369994 +0000
+++ jvm.options 2017-05-02 23:51:36.788119994 +0000
@@ -19,8 +19,8 @@
 # Xms represents the initial size of total heap space
 # Xmx represents the maximum size of total heap space

--Xms2g
--Xmx2g
+-Xms512m
+-Xmx512m

 ################################################################
 ## Expert settings

実際にどの程度の値に設定すれば良いのかについては、以下のドキュメントに記載されている。

www.elastic.co

ざっくり要約すると…(Powered by Google 翻訳)

  • minimum heap size (Xms) と maximum heap size (Xmx) は同じ値にしましょう
  • Elasticsearch で使用可能なヒープサイズが多いほど、キャッシュに使用できるメモリが増えるばい
  • ただし、ヒープサイズを割り当て過ぎると長い GC が発生する可能性があることに注意しませう
  • maximum heap size (Xmx) は物理メモリの 50% を超えないようにして、カーネルファイルシステムのキャッシュ用に十分な物理メモリが残っていることを確認しませう
  • maximum heap size (Xmx) を 32GB に近い値に設定しない方が良いです(超意訳ですいません … compressed oops の絡み)
  • じゃあ、Xmx の最大値だと 26GB 〜 30GB くらいにしておくと安全でせう(超意訳ですいません)

がっつりメモリがある場合でも、メモリの割り当てには注意しましょうってことですな。

有難うございました

改めて JVM について勉強が必要な事がわかったので、今回は「技あり」ってことで。

2017 年 04 月 30 日(日)

ジョギング

  • 香椎浜 x 2 周
  • ジョギングには良い季節になってきた(むしろ暑いくらい)

日課

  • (腕立て x 30 + 腹筋 x 30) x 3

うなぎ

お昼にうなぎを食べる。

美味しゅうございました。

俺は Linux コンテナについてなんにも解っていなかった 〜 haconiwa で学ぶ Linux コンテナ (1) 〜

追記

@udzura さんからコメントを頂きました!

有難うございました!

直近の Docker 界隈について

以下のようなキーワードがインターネット上を駆け巡っている気がする。

自分自身、これのらのキーワードについて中身を全く理解出来ていない。

また、Docker を触る機会も減っているのも事実で、やばいな〜、やばいな〜と思いながら、ゴールデンウィーク突入となったので、改めて、Docker というよりも Linux コンテナ技術について勉強してみたいと思う。

Linux コンテナ

参考

speakerdeck.com

Linux コンテナとは

Linux カーネルの機能で以下のような機能を提供する。

  • 隔離された空間でプロセスを実行する
  • プロセスに対してリソース制限を設定する

Linux コンテナを構成する主な機能

Linux コンテナはカーネルに含まれる以下のような機能を利用して実装されている。

名前 役割
namespace OS リソースの隔離(プロセスをグループ化して他のリソースと隔離)
cgroup ホストの物理リソースに対する制限(グループ化したプロセス対するリソース制限)
capability root 権限をプロセスやファイルに割り当てる

上記以外にも Bind mount/chrootResource limit(rlimit) 及び setuid/setgid 等の機能も利用されている。

OS リソース毎の Namespace

OS リソース毎に以下のような Namespace が提供されている。

  • Mount Namespace
  • UTS Namespace
  • PID Namespace
  • IPC Namespace
  • User Namespace
  • Network Namespace
  • cgroup Namespace

各 Namespace はカーネルのバージョンを追う毎に追加実装されている。

cgroup サブシステム

cgroup は cgroup ファイルシステムという仮想的なファイルシステムを使って操作し、Namespace 同様に以下のようなサブシステムと呼ばれる機能でリソースを扱う。

  • cpu
  • cpuacct
  • cpuset
  • device
  • freezer
  • memory
  • blkio

他にも hugetlb や perf_event 等が提供されている。

こんなにざっくりでは、Linux コンテナは語れないと思うけど

最低限、上記のようなキーワードだけはしっかりと覚えておきたい。

haconiwa

haconiwa とは

github.com

  • @udzura さんがメインとなって実装されている mruby で実装された Linux コンテナのランタイム
  • Ruby DSLLinux コンテナの設定を記述することが出来て、自分だけの Linux コンテナを作成することが出来る

haconiwa で何が出来ると?

haconiwa 導入

haconiwa は mruby-cli でビルドされたバイナリで配布されており、packagecloud にてパッケージ配布されているので、以下のようにインストールする。

#
# Ubunt/xenial に導入する
#
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"

#
# 事前に lxc を導入済み
#
$ dpkg --list | grep lxc
ii  liblxc1                          2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (library)
ii  lxc                              2.0.7-0ubuntu1~16.04.2                     all          Transitional package for lxc1
ii  lxc-common                       2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (common tools)
ii  lxc-templates                    2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (templates)
ii  lxc1                             2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools
ii  lxcfs                            2.0.6-0ubuntu1~16.04.1                     amd64        FUSE based filesystem for LXC
ii  python3-lxc                      2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (Python 3.x bindings)

#
# haconiwa パッケージの導入
#
$ curl -s https://packagecloud.io/install/repositories/udzura/haconiwa/script.deb.sh | sudo bash
$ sudo apt-get install haconiwa=0.8.5-1

#
# ヘルプの確認
#
ubuntu@ubuntu-xenial:~$ haconiwa
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.

はじめての haconiwa (1)

以下のように haconiwa new を実行すると定義ファイルの雛形を生成する事が出来る。

haconiwa new \
--name=my-first-container \
--root=/var/lib/haconiwa/my-first-container my-first-container.haco

以下のように出力され、my-first-container.haco というファイルがカレントディレクトリに生成されている。

create  my-first-container.haco

.haco ファイルの中身は以下のようになっている。

$ grep -v -e '^\s*#' -e '^\s*$' my-first-container.haco
Haconiwa.define do |config|
  config.name = "my-first-container"
  config.init_command = "/bin/bash"
  root = Pathname.new("/var/lib/haconiwa/my-first-container")
  config.chroot_to root
  config.bootstrap do |b|
    b.strategy = "lxc"
    b.os_type  = "alpine"
  end
  config.provision do |p|
    p.run_shell <<-SHELL
apk add --update bash
    SHELL
  end
  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

はじめての haconiwa (2) 〜 コンテナ作成 〜

以下のように create オプションを指定して haconiwa を実行すると、lxc-create が実行されて alpine linux のコンテナの作成が開始される。

sudo haconiwa create my-first-container.haco

以下のように出力される。

ubuntu@ubuntu-xenial:~$ sudo haconiwa create my-first-container.haco
Creating rootfs of my-first-container...
Start bootstrapping rootfs with lxc-create...
[bootstrap.lxc]:        Obtaining an exclusive lock... done

...

Command success: lxc-create -n my-first-container -t alpine --dir /var/lib/haconiwa/my-first-container exited 0
Success!
Start provisioning...
Running provisioning with shell script...
[provison.shell-1]:     + apk add --update bash
[provison.shell-1]:     fetch http://mirror.yandex.ru/mirrors/alpine//v3.5/main/x86_64/APKINDEX.tar.gz
[provison.shell-1]:     (1/5) Installing ncurses-terminfo-base (6.0-r7)
[provison.shell-1]:     (2/5) Installing ncurses-terminfo (6.0-r7)
[provison.shell-1]:     (3/5) Installing ncurses-libs (6.0-r7)
[provison.shell-1]:     (4/5) Installing readline (6.3.008-r4)
[provison.shell-1]:     (5/5) Installing bash (4.3.46-r5)
[provison.shell-1]:     Executing bash-4.3.46-r5.post-install
[provison.shell-1]:     Executing busybox-1.25.1-r0.trigger
[provison.shell-1]:     OK: 14 MiB in 21 packages
Command success: /bin/sh -xe exited 0
Success!

はじめての haconiwa (3) 〜 コンテナ起動 〜

以下のように run 又は start オプションを指定して haconiwa を実行するとコンテナが起動する!!

$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=1893
bash-4.3# ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
    2 root       0:00 ps
bash-4.3# exit
exit
Container(1893) finish detected: #<Process::Status: pid=1893,exited(0)>
Container successfully exited: #<Process::Status: pid=1893,exited(0)>
One of supervisors finished: 1892, #<Process::Status: pid=1892,exited(0)>

$ sudo haconiwa start my-first-container.haco
Container fork success and going to wait: pid=1906
bash-4.3# ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
    2 root       0:00 ps
bash-4.3# exit
exit
Container(1906) finish detected: #<Process::Status: pid=1906,exited(0)>
Container successfully exited: #<Process::Status: pid=1906,exited(0)>
One of supervisors finished: 1905, #<Process::Status: pid=1905,exited(0)>

ちなみに、起動したコンテナからインターネットへのアクセスだって OK 牧場。

$ sudo haconiwa start my-first-container.haco
Container fork success and going to wait: pid=1919
bash-4.3#
bash-4.3# ping www.yahoo.com -c 3
PING www.yahoo.com (106.10.139.246): 56 data bytes
64 bytes from 106.10.139.246: seq=0 ttl=63 time=137.592 ms
64 bytes from 106.10.139.246: seq=1 ttl=63 time=158.620 ms
64 bytes from 106.10.139.246: seq=2 ttl=63 time=106.147 ms

--- www.yahoo.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 106.147/134.119/158.620 ms
bash-4.3#

haconiwa で学ぶ Linux コンテナ

.haco ファイル再掲

ここからは、以下の .haco ファイルを利用して Linux コンテナを弄ってみたいと思う。

Haconiwa.define do |config|
  config.name = "my-first-container"
  config.init_command = "/bin/bash"
  root = Pathname.new("/var/lib/haconiwa/my-first-container")
  config.chroot_to root
  config.bootstrap do |b|
    b.strategy = "lxc"
    b.os_type  = "alpine"
  end
  config.provision do |p|
    p.run_shell <<-SHELL
apk add --update bash
    SHELL
  end
  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

namespace を弄る前に

Namespace の機能についておさらい。

namespace 機能
mount Namespace 内の mount / umount が他の Namespace に影響を与えないようにする
ipc SysV IPC オブジェクトや POSIX メッセージキューの隔離
uts hostnameuname の結果を分離
pid PID 空間の分離、新しい PID Namespace では PID 1 から始まる

utspid について弄ってみる。

namespace uts

以下のように config.namespace.unshare "uts"コメントアウト

ubuntu@ubuntu-xenial:~$ diff -u my-first-container.haco.bk my-first-container.haco
--- my-first-container.haco.bk  2017-04-29 23:35:21.313501998 +0000
+++ my-first-container.haco     2017-04-29 23:35:32.883713999 +0000
@@ -59,7 +59,7 @@
   # The namespaces to unshare:
   config.namespace.unshare "mount"
   config.namespace.unshare "ipc"
-  config.namespace.unshare "uts"
+  # config.namespace.unshare "uts"
   config.namespace.unshare "pid"

   # You can use existing namespace via symlink file. e.g.:

何が起きるのか。

ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial
ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3449
bash-4.3# hostname
ubuntu-xenial

utshostnameuname の結果を隔離する namespace であるが、unshare を行っていない為、hostname の結果が隔離されておらず、ホストと同じ hostname の結果を返している状態。

ちなみに、この状態でコンテナ内からホスト名を変更してみると、以下のように怒られる。

bash-4.3# hostname foo
hostname: sethostname: Operation not permitted

これは、sethostname(2)

EPERM
sethostname() において、呼び出した人が CAP_SYS_ADMIN ケーパビリティ (capability) を持っていなかった。

とある事から察するに、haconiwa においてコンテナ内部からのホスト名変更は Linux capability により制御されていると思われる。

改めて、config.namespace.unshare "uts" をコメントインして haconiwa run してみる。

ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial
ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3651
bash-4.3# hostname
my-first-container

hostname コマンドの実行結果がホスト側とコンテナ内で異なっており、コンテナに隔離された状態となっている。

尚、unshare コマンドで uts のみを分離した場合には以下のような挙動となり、隔離した環境において実施した hostname コマンドの影響は限定的となっていることが解る。

#
# uts namespace を隔離
#
ubuntu@ubuntu-xenial:~$ sudo unshare --uts -- /bin/bash
root@ubuntu-xenial:~# hostname
ubuntu-xenial
root@ubuntu-xenial:~# hostname foo
root@ubuntu-xenial:~# hostname
foo

#
# 別端末で hostname を確認(hostname 変更の影響は出ていない)
#
ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial
ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial

namespace pid

以下のように config.namespace.unshare "pid"コメントアウト

$ diff -u my-first-container.haco.bk my-first-container.haco
--- my-first-container.haco.bk  2017-04-29 23:35:21.313501998 +0000
+++ my-first-container.haco     2017-04-29 23:56:19.213720735 +0000
@@ -60,7 +60,7 @@
   config.namespace.unshare "mount"
   config.namespace.unshare "ipc"
   config.namespace.unshare "uts"
-  config.namespace.unshare "pid"
+  # config.namespace.unshare "pid"

   # You can use existing namespace via symlink file. e.g.:
   # config.namespace.enter "net", via: "/var/run/netns/sample001"

何が起きるのか…

ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3596
bash-4.3# ps aux | less

PID   USER     TIME   COMMAND
    1 root       0:13 {systemd} /sbin/init
    2 root       0:00 [kthreadd]
    3 root       0:00 [ksoftirqd/0]

...

3596 root       0:00 /bin/bash
3602 root       0:00 [kworker/u4:2]
3605 root       0:00 ps aux
3606 root       0:00 less

ホスト側のプロセスが丸見え状態になっている。

改めて、config.namespace.unshare "pid" をコメントインして haconiwa run してみる。

ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3623
bash-4.3# ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
    2 root       0:00 ps aux

プロセス ID は 1 から開始され、ホスト側のプロセス ID を見ることは出来ない状態になっておりコンテナに隔離された状態となっている。

俺は Linux コンテナについてなんにも解っていなかった(まとめ)

haconiwa は

  • Ruby DSL でコンテナ定義を書くことが出来る Linux コンテナランタイム
  • 手軽に色々な状態のコンテナを作成することが出来るので Linux コンテナを学ぶのに打ってつけ

Linux コンテナ

  • 自分は本当になんにも解っていなかった
  • Linux コンテナとは namespace による OS リソースの隔離、cgroup によるマシンリソースの制限等の Linux カーネルの機能を寄せ集めて実装されている
  • Docker は良しなに Linux コンテナをラップして使いやすくしただけ(それが凄いことなんだけど)で、コンテナを動かすのに Docker は必ずしも必要無いんだなってことが解った

次回は

cgroup や capability に触れられれば良いかなと考えている。

参考

Linux コンテナ神資料

Linux コンテナを語る上で欠かせない神資料達。

haconiwa 神資料

haconiwa を触る上で欠かす事の出来ない神資料達。

2017 年 04 月 29 日(土)

ジョギング

日課

  • (腕立て x 30 + 腹筋 x 30) x 3

義父

あまり状態が良くないとのこと。

我々と話す時は声にも張りがあって、状態が悪いのが嘘のようであるが、先週会った時とくらべても痩せているような気がした。