devstackをvagrantで構築する(2016-01-03)

OpenstackのAPIで遊ぼうと思って、vagrantでdevstackを構築することにしたのだけど、結構てこずったので記録しておく。

VMの作成

Vagrantfile

他のVMからAPIを使いたいので、private networkを追加している。

[ruby]
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/trusty64"

config.vm.network "forwarded_port", guest: 5000, host: 5000
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "private_network", ip: "192.168.33.20"

config.vm.provider "virtualbox" do |vb|
vb.name = ‘devstack’
vb.cpus = 2
vb.memory = 8192
end

config.vm.provision "shell", path: "provision.sh"
end
[/ruby]

provision.sh

たぶん、ここで全て構築できるのだけど、手順自体がよくわかっていなかったので、今回は最低限の内容にした。

[text]
sudo apt-get -y update
sudo apt-get -y install git vim
[/text]

devstack構築

stackユーザ作成

NOPASSWDでsudoできるstackユーザを作成しておく。ここはvagrantのprovisionでやってしまってよさそう。

[text]
$ sudo adduser stack
$ stack@vagrant-ubuntu-trusty-64:~$ sudo cat /etc/sudoers.d/50_stack
stack ALL=(ALL) NOPASSWD:ALL
Defaults:stack !requiretty
$ sudo chmod 440 /etc/sudoers.d/50_stack
[/text]

devstack構築

[text]
# stackユーザで操作する
$ sudo su – stack

# libertyを使うことにする
$ git clone https://github.com/openstack-dev/devstack.git
$ cd devstack
$ git checkout -b stable/liberty origin/stable/liberty

# 設定ファイル。サンプルをベースにして、Neutronを使うように修正。
$ cp samples/local.conf .
$ cat local.conf
[[local|localrc]]
SERVICE_TOKEN=azertytoken
ADMIN_PASSWORD=nomoresecrete
DATABASE_PASSWORD=stackdb
RABBIT_PASSWORD=stackqueue
SERVICE_PASSWORD=$ADMIN_PASSWORD

LOGFILE=$DEST/logs/stack.sh.log
LOGDAYS=2

SWIFT_HASH=66a3d6b56c1f479c8b4e70ab5c2000f5
SWIFT_REPLICAS=1
SWIFT_DATA_DIR=$DEST/data

HOST_IP=192.168.33.20 # Keystoneに登録されるendpointはここで指定したものが使われる模様

disable_service n-net
ENABLED_SERVICES+=,q-svc,q-dhcp,q-meta,q-agt,q-l3,neutron
PUBLIC_INTERFACE=eth1

# コマンドを実行して待つ。
$ ./stack.sh
[/text]

30分程すれば使えるようになる。http://localhost:8080/ からHorizonが使える。また、同じprivate networkであればAPIも使える。local.confでHOST_IPを指定していないとprivate networkではなくhost onlyなものが使われるみたい。

[text]
$ curl -v http://192.168.33.20:5000/v3/auth/tokens
* Hostname was NOT found in DNS cache
* Trying 192.168.33.20…
* Connected to 192.168.33.20 (192.168.33.20) port 5000 (#0)
> GET /v3/auth/tokens HTTP/1.1
> User-Agent: curl/7.35.0
> Host: 192.168.33.20:5000
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Sun, 03 Jan 2016 02:22:10 GMT
* Server Apache/2.4.7 (Ubuntu) is not blacklisted
< Server: Apache/2.4.7 (Ubuntu)
< Vary: X-Auth-Token
< x-openstack-request-id: req-96ba3c65-3132-4f73-b910-e5eb674cda53
< WWW-Authenticate: Keystone uri="http://192.168.33.20:5000"
< Content-Length: 162
< Content-Type: application/json
<
* Connection #0 to host 192.168.33.20 left intact
{"error": {"message": "The request you have made requires authentication. (Disable debug mode to suppress these details.)", "code": 401, "title": "Unauthorized"}}%
[/text]

RancherOSの起動時の仕組み

※2015-03-17にシステムコンテナの起動を追加、他にも加筆・修正。

Tiny Docker Operating Systems | Docker Blogを見たので触ってみた。

RancherOSはDockerコンテナを動かすためのミニマムなLinuxディストリビューションで、以下のような特徴を持つ。

  • PID 1がDockerで、カーネルが起動する時の最初のプロセスである
  • システム用のDockerとユーザ用のDockerがいる
  • Goで書かれた独自のinitを持つ
  • 非常にコンパクトである

PID1のDockerはシステム用のDocker(System Docker)と呼ばれていて、udevやdhcpd、ユーザ用のDocker(User Docker)を動かす。
要はsysvinitやsystemdの代わりにDockerを使っている、という感じだろうか。
RancherOSのサイトでもsystemdのような複雑なinitシステムを排除する、と言っている。
また、異なるディストリビューションのコンソールを簡単に使うこともできるらしい。
サイズも非常に小さく(20M)、起動も速い。

RancherOSの概要についてはAmazon EC2でRancherOSを触ってみたREADME.mdを見れば分かりやすい。
また、Vagrantで簡単に試すことができる。

今回は、どのようにしてDockerデーモンをPID1のプロセスにしているのか、またシステムコンテナの起動部分を追ってみる。なお、RahcnerOSのリポジトリはここ

まず、起動の流れについて図示する。

2015-03-13_rancheros_init

以下のように、大きく3段階に分かれており、1つずつ見ていく。

  1.  initrdのブート・/initの実行
  2. 初期化・SystemDockerの実行
  3. システムコンテナの実行

1. /boot/initrdを使ってブート

ビルド時にinitrdを作るが、その際にBusyBoxの/initをRancherOS独自に実装したものに置き換えている。これはGoで書かれている。
Dockerの起動をしたりRancherOS用のコマンドを提供している。

置き換えは以下のような流れで行われる。

  1. script/buildの中でgo buildしてinitとなるバイナリ(bin/rancheros)を作る
  2. script/packageで作成したbin/rancherosをinitrd/initにコピーする
  3. artifact/initrd以下をcpio形式でアーカイブし、さらにlzmaで圧縮し、initrdファイルを作成する

isolinux.cfgは以下のようになっている。/boot/initrdのinitは/bin/rancherosなので、起動時にmain.goのmain関数が実行される。

[text]
default rancheros
label rancheros
kernel /boot/vmlinuz
initrd /boot/initrd
append quiet rancher.password=rancher rancher.modules=[mptspi]
[/text]

2. init/init.go:MainInit関数の実行

main.goのmain関数では、RancherOS用のコマンドを登録する。
コマンドのパスがキーで、実行する関数が値だ。

[go]
func main() {
registerCmd("/init", osInit.MainInit)
registerCmd(osInit.SYSINIT, sysinit.Main)
registerCmd("/usr/bin/system-docker", systemdocker.Main)
registerCmd("/sbin/poweroff", power.PowerOff)
registerCmd("/sbin/reboot", power.Reboot)
registerCmd("/sbin/halt", power.Halt)
registerCmd("/sbin/shutdown", power.Main)
registerCmd("/usr/bin/respawn", respawn.Main)
registerCmd("/usr/sbin/rancherctl", control.Main)
registerCmd("/usr/bin/cloud-init", cloudinit.Main)
registerCmd("/usr/sbin/netconf", network.Main)

if !reexec.Init() {
log.Fatalf("Failed to find an entry point for %s", os.Args[0])
}
}
[/go]

main.goのregisterCmd。cmdが「/usr/bin/system-docker」の場合、「/usr/bin/system-docker」と「system-docker」、「./system-docker」の3種類のキーで関数を登録する。

[go]
func registerCmd(cmd string, mainFunc func()) {
log.Debugf("Registering main %s", cmd)
reexec.Register(cmd, mainFunc)

parts := strings.Split(cmd, "/")
if len(parts) == 0 {
return
}

last := parts[len(parts)-1]

log.Debugf("Registering main %s", last)
reexec.Register(last, mainFunc)

log.Debugf("Registering main %s", "./"+last)
reexec.Register("./"+last, mainFunc)
}
[/go]

実際に登録しているのはpkg/reexec/reexec.goのRegister関数。
registeredInitializersに名前と関数を登録する。

[go]
var registeredInitializers = make(map[string]func())

// Register adds an initialization func under the specified name
func Register(name string, initializer func()) {
if _, exists := registeredInitializers[name]; exists {
panic(fmt.Sprintf("reexec func already registred under name %q", name))
}

registeredInitializers[name] = initializer
}
[/go]

main.goのmain関数に戻る。
registerCmdを全て実行したらreexec.Init()を実行。

[go]
func main() {
registerCmd("/init", osInit.MainInit)
registerCmd(osInit.SYSINIT, sysinit.Main)
registerCmd("/usr/bin/system-docker", systemdocker.Main)
registerCmd("/sbin/poweroff", power.PowerOff)
registerCmd("/sbin/reboot", power.Reboot)
registerCmd("/sbin/halt", power.Halt)
registerCmd("/sbin/shutdown", power.Main)
registerCmd("/usr/bin/respawn", respawn.Main)
registerCmd("/usr/sbin/rancherctl", control.Main)
registerCmd("/usr/bin/cloud-init", cloudinit.Main)
registerCmd("/usr/sbin/netconf", network.Main)

if !reexec.Init() {
log.Fatalf("Failed to find an entry point for %s", os.Args[0])
}
}
[/go]

reexec.goのinit関数では実行するコマンドに対応する関数を実行する。
os.Args[0]は実行するコマンド自身。

[go]
func Init() bool {
initializer, exists := registeredInitializers[os.Args[0]]
if exists {
initializer()

return true
}
return false
}
[/go]

つまり、起動時にinitが実行されると、それに対応する関数であるosInit.MainInitが実行される。
osInit.MainInitはosInit.RunInitを呼び出していて、これが行っているのはinitFuncsとDockerの実行である。
InitFuncは以下のような処理を行っている。

  • ディレクトリの作成
  • マウント
  • resolve.confの設定
  • シンボリックリンクの作成
    • /sbin/rancheros-sysinit -> /init
  • カーネルモジュールの有効化
  • ReadOnlyで/を再度マウント
  • sysInitの実行
    • /sbin/rancheros-sysinitを外部コマンドとして実行

[go]
func RunInit() error {
var cfg config.Config

os.Setenv("PATH", "/sbin:/usr/bin")
os.Setenv("DOCKER_RAMDISK", "true")

initFuncs := []config.InitFunc{
func(cfg *config.Config) error {
return createDirs(dirs…)
},
func(cfg *config.Config) error {
log.Info("Setting up mounts")
return createMounts(mounts…)
},
func(cfg *config.Config) error {
newCfg, err := config.LoadConfig()
if err == nil {
newCfg, err = config.LoadConfig()
}
if err == nil {
*cfg = *newCfg
}

if cfg.Debug {
cfgString, _ := cfg.Dump()
if cfgString != "" {
log.Debugf("Config: %s", cfgString)
}
}

return err
},
mountCgroups,
setResolvConf,
createSymlinks,
extractModules,
loadModules,
mountState,
func(cfg *config.Config) error {
return createDirs(postDirs…)
},
func(cfg *config.Config) error {
return createMounts(postMounts…)
},
func(cfg *config.Config) error {
return cfg.Reload()
},
remountRo,
sysInit,
}

if err := config.RunInitFuncs(&cfg, initFuncs); err != nil {
return err
}

return execDocker(&cfg)
}
[/go]

createSymlinksでは/init、つまり自分自身へのリンクを/sbin/rancheros-sysinitとして作成し、sysInit関数で外部コマンドとしてそれを実行している。/initはrancheros-sysinitとして実行された場合、cmd/sysinit/sysinit.goのMain関数を実行する。
また、コマンドの実行終了は待たずに即リターンしている。

[go]
func sysInit(cfg *config.Config) error {
args := append([]string{SYSINIT}, os.Args[1:]…)

var cmd *exec.Cmd
if util.IsRunningInTty() {
cmd = exec.Command(args[0], args[1:]…)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
} else {
cmd = exec.Command(args[0], args[1:]…)
}

if err := cmd.Start(); err != nil {
return err
}

return os.Stdin.Close()
}
[/go]

その後、init.goのexecDockerではsyscall.Execを使ってDockerを起動する。これがPID 1のプロセス、つまりSystem Dockerになる。

[go]
func execDocker(cfg *config.Config) error {
log.Info("Launching System Docker")
if !cfg.Debug {
output, err := os.Create("/var/log/system-docker.log")
if err != nil {
return err
}

syscall.Dup2(int(output.Fd()), int(os.Stdout.Fd()))
syscall.Dup2(int(output.Fd()), int(os.Stderr.Fd()))
}

os.Stdin.Close()
return syscall.Exec(DOCKER, cfg.SystemDockerArgs, os.Environ())
}
[/go]

3. cmd/sysinit/sysinit.go:Main関数の実行

SystemDockerを起動する前に外部コマンドとして/sbin/rancheros-sysinitを実行したが、このコマンドはシステムコンテナの実行を行っている。
標準で実行するのは以下のコンテナである。

  • system-volumes
    • /var/runや/var/log等をマウント
  • command-volumes
    • rahcnerctlやsystemdocker、cloud-init等を/initとしてマウント
  • user-volumes
    • /homeと/optをマウント
  • udev
  • network
  • cloud-init
  • デーモンとして実行
    • ntp
    • syslog
    • userdocker
    • console

最後に実行するconsoleコンテナはsshdやttyの実行も行っていて、ログインするとconsoleコンテナの中に入ることになる。
consoleコンテナはcommand-columesやuser-volumes、system-volumesをマウントしているのでrancherctlのようなコマンドを使うことができる。

まとめ

Goで書いた独自のinitを持ち、PID1番のプロセスがDockerであるRancherOSの起動時の挙動について見てみた。
Dockerに依存していて今後どうなるかわからないけれど、おもしろいと思う。
また、今回はSystem Dockerの起動後の動きは見ていないのでそちらもチェックししたい。 本記事に加筆した。

Go言語の構造体でタグを使う時のメモ

型の後ろに「xml:"hoge"」と書けば,Marshal・Unmarshal時に対応づけてくれる.

[go]
type Foo struct {
Hoge string xml:"hoge"
}
[/go]

jsonもOK.

[go]
type Foo struct {
Hoge string json:"hoge"
}
[/go]

xmlとjson両方設定する場合はスペースで区切る.

[go]
type Foo struct {
Hoge string xml:"hoge" json:"hoge"
}

type Foo struct {
Hoge string xml:"hoge" json:"hoge"
}
[/go]

DockerのストレージバックエンドにBtrfsが使えるようになってたので試した

みなさんこんにちは。 最近Docker Meetup in Tokyo #1の参加を逃したりImmutable Infrastructure Conference #1の参加申し込みに出遅れたりしてがっくりしている@r_takaishiです。

さて、今回はDockerのv0.8でストレージドライバに新しくBtrfsが追加されたので、これを試してみました。

Btrfsをストレージとして使うための準備

まずは、DockerがBtrfsをストレージとして使えるようにします。 なお、環境ですが、Vagrant + Ubuntu13.10 を使用しました。 Dockerのインストールは公式ドキュメントに従っています。 Btrfsを使うための手順は以下の通りです。

  1. Btrfsなパーティションを作成 & マウントする
  2. Dockerデーモンの起動オプションでストレージドライバとデータ保存先のディレクトリを指定する
  3. Dockerデーモンを再起動する

Btrfsなパーティションを作成 & マウントする

適当にストレージを追加して、mkfs.btrfsでファイルシステムを作成、任意のディレクトリにマウントするだけです。 今回は、「/opt/docker」にマウントしておくことにします。

Dockerデーモンの起動オプションでストレージドライバとデータ保存先のディレクトリを指定する

Dockerのデーモンは標準だとストレージドライバにaufsを、データの保存先には「/var/lib/docker」を使用します。 これを、ストレージドライバにbtrfsを、データの保存先には「/opt/docker」を使用するように設定してあげます。 サービスとして起動しているので、その設定ファイルである「/etc/default/docker」内の「DOCKER_OPTS」を以下のように書き換えます。

[bash]
DOCKER_OPTS="-s btrfs -g /opt/docker"
[/bash]

Dockerデーモンを再起動する

再起動しましょう。これで、ストレージドライバにBtrfsを使うようになります。

[bash]
sudo service docker restart
[/bash]

aufsとbtrfsで、コンテナのビルド速度を比較する

Btrfsストレージドライバを触っている時、ビルドに時間がかかるような気がしたので、aufsと比較してみることにしました。 比較する操作は以下の通り。

  1. ubuntu:13.10をベースにイメージを作成(ubuntu:13.10はあらかじめpullしておきます)
  2. 1で作成したイメージをベースにイメージを作成
  3. 2で作成したイメージをベースにイメージを作成
  4. 2〜3を100回繰り返す

これらの操作を行うスクリプトをRubyで書き、1〜4に要する時間を測定しました(測定回数は一応5回)。 その結果、以下のように大きな差がありました。

  • Btrfs:平均256秒
  • aufs: 平均47秒

実に5倍の差があります。現時点で、Btrfsストレージドライバは実験的なものではあるのですが、これ程差があるとは思いませんでした。 仮想マシンを作成する事に比べるとはるかに高速ですが、それでもインフラをCIするような、ビルドを何度も繰り返すことになるケースだと遅く感じる事がありそうです。 今の所aufsの使用で困っている事はないので、Btrfsストレージドライバをメインに使うのはしばらく先にしようと思います。 Btrfsストレージドライバが遅い理由までは今回は調べることはできていません。現状の実装の影響なのか、そもそも仕組みからして遅い、という可能性もあります。 後、これもきちんとは調べていないのですが、aufsで100個近くヒストリを繋いでいると、後半にいくにつれて若干ビルドに時間がかかるようになっていたような気がします。 今後、調べてみたいですね。

備考

測定に使用したスクリプトは以下の通りです。出力が汚いけど気にしない。

 

[ruby]
— coding: utf-8 —

require ‘benchmark’ require ‘systemu’

5.times do |count|
# Btrfs用。作成したイメージ等を全削除する。 #sudo service docker stop #cd /opt/docker && sudo btrfs subvolume list . | awk ‘{print $9}’ | xargs -I{} sudo btrfs subvolume delete {} #sudo rm -rf /opt/docker/*
# aufs用。作成したイメージ等を全削除する。 sudo docker images | awk ‘{print $3}’ | xargs -I{} sudo docker rmi {} sudo service docker stop sleep 2 sudo rm -rf /var/lib/docker
# サービスの起動と初期イメージの取得 sudo service docker start sleep 2 systemu "sudo docker pull ubuntu:13.10"
# ビルドの繰り返し(ここの時間を測定) Benchmark.bm do |x| x.report("#{count}: ") { base_id=” status, stdout, stderr = systemu "echo ‘FROM ubuntu:13.10\nRUN echo init >> /log’ | sudo docker build -t base -" stdout.each_line do |line| if line =~ /Successfully built (.*)\n/ base_id=$1 end end

100.times do |num|
status, stdout, stderr = systemu "echo ‘FROM #{base_id}\nRUN echo #{num} >> /log’ | sudo docker build -t #{num} -"
stdout.each_line do |line|
# puts line
if line =~ /Successfully built (.*)\n/
base_id=$1
end
end
end
end
[/ruby]

org-modeのアーカイブ先ファイル名を変更する

org-modeでタスクを管理していると、だんだんファイルサイズが大きくなる。 そのため、「org-archive-subtree」を使って完了したタスクをアーカイブしてファイルのサイズを小さくするのだけど、数年使っているとアーカイブ先のファイルサイズが大きくなる。 そこで、このアーカイブ先のファイル名を変更して肥大化を防ぐ。

ファイル名は標準では「%s_archive」である。「todo.org」の場合は「todo.org_archive」だ。 この名前は「org-get-location-archive-location」で決定される。 バッファ内に「#+ARCHIVE: 文字列」というオプションを設定している場合はその名前が使われる。 設定していない場合は「org-archive-location」変数が使われる。 そこで、次のように設定する。

scm
(setq org-archive-location (concat "%s_archive_" (format-time-string "%Y" (current-time))))
接尾辞として「_archive_2014」のように、その年をつけることで、毎年違うファイルにアーカイブできる。