hidemium's blog

日々学んだことをアウトプットする。

ChefからDockerコンテナ内の起動プロセスを追加する

Dockerはコンテナの起動時に、initプロセスではなく、CMDで指定したプロセスから実行されるため、通常は1つしかプロセスを起動できない制約があります。しかし、プロセス管理ツールのsupervisorを使用すれば、コンテナ内で複数のプロセスを起動することができます。

前回、Dockerfileを使ってsupervisorからsshdとMySQLのプロセスを起動させることを確認できました。そこで、Chefを使ってsupervisorに起動プロセスの定義を追加することで、コンテナ内の起動プロセスを追加できないか試してみました。

構成

Ubuntu 12.04: サーバ構築対象
Ubuntu 12.04はDocker 0.10上で動作しています。

Dockerfileの作成

Dockerfileの構成は以下の通りです。

sshd
├──Dockerfile
├──sources.list      ... ミラーサイト一覧
├──id_rsa.pub        ... Chef workstationの公開鍵
└──supervisord.conf  ... supervisorの定義ファイル

Dockerfileを以下のように作成します。
後でMySQLを起動できるように、「RUN dpkg-divert~」を設定しています。
supervisorの定義ファイルは、/etc/supervisor/conf.d/配下にも置くことができるので、「RUN mkdir -p /etc/supervisor/conf.d/」でディレクトリを作成しています。

$ vi Dockerfile
FROM ubuntu:12.04

MAINTAINER hidemium

# Ubuntu update
ADD sources.list /etc/apt/sources.list
RUN apt-get -y update

# Hack for initctl not being available in Ubuntu
RUN dpkg-divert --local --rename --add /sbin/initctl
RUN ln -sf /bin/true /sbin/initctl

# ssh install
RUN apt-get -y install openssh-server
RUN apt-get -y install python-setuptools
RUN apt-get clean
RUN easy_install supervisor

RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd

# sshd config
ADD id_rsa.pub /root/id_rsa.pub
RUN mkdir /root/.ssh/
RUN mv /root/id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed -i -e '/^UsePAM\s\+yes/d' /etc/ssh/sshd_config

# supervisor config
RUN mkdir -p /var/log/supervisor
RUN mkdir -p /etc/supervisor/conf.d/
ADD supervisord.conf /etc/supervisord.conf

# Expose ports.
EXPOSE 22

# Define default command.
CMD ["supervisord", "-n"]

supervisorの定義ファイルは、デフォルトの定義にsshdの起動を追加したものにしています。

$ vi supervisord.conf
[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)

[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files = /etc/supervisor/conf.d/*.conf

[program:sshd]
command=/usr/sbin/sshd -D
autostart=true
autorestart=true

Chefレシピの作成例

それでは、Chef側について見ていきます。
今回、apache2とMySQLについて試してみました。

apache2

cookbookの構成は以下の通りです。

site-cookbook
└──apache2
      └──recipes
      │    └──default.rb
      └──templates
            └──default
                  └──apache2.conf.erb ... supervisorの定義ファイル

recipesファイル(抜粋)は以下の通りです。

Chef側では、templateファイルとしてsupervisorの定義ファイルを用意します。
定義ファイルを配置後、supervisorctl reloadコマンドによりプロセスを起動します。
プログラムによっては、supervisorctl <サービス名> startコマンドでも起動できない場合があるため、supervisorctl reloadコマンドでsupervisorに定義されたプロセスをすべて再読み込みさせています。
ChefはSSH接続で実行していますが、supervisorの再読み込みでSSHが切断されることはありませんでした。

$ vi site-cookbooks/apache2/recipes/default.rb
:
package "apache2" do
    action :install
end

template "apache2.conf" do
    path "/etc/supervisor/conf.d/apache2.conf"
    owner "root"
    group "root"
    mode "0644"
    source "apache2.conf.erb"
end

bash "supervisorctl reload" do
    code   "supervisorctl reload"
    action :run
end
:

templateファイルは以下の通りです。

$ vi site-cookbooks/apache2/templates/default/apache2.conf.erb
[program:apache2]
command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND"

MySQL

cookbookの構成は以下の通りです。

site-cookbook
└──mysql
      └──recipes
      │    └──default.rb
      └──templates
            └──default
                  └──mysqld.conf.erb ... supervisorの定義ファイル

MySQLを追加する場合のrecipesファイル(抜粋)は以下の通りです。

上記と同じ構成となっています。
supervisorctl reloadコマンド後に、sleepを入れいるのは、mysqldの起動に時間がかかるため、mysqlコマンドを実行しても接続できないためです。

$ vi site-cookbooks/mysql/recipes/default.rb
:
package "mysql-server" do
    action :install
end

template "mysqld.conf" do
    path "/etc/supervisor/conf.d/mysqld.conf"
    owner "root"
    group "root"
    mode "0644"
    source "mysqld.conf.erb"
end

bash "supervisorctl reload" do
    code   "supervisorctl reload; sleep 3"
    action :run
end
:

templateファイルは以下の通りです。

$ vi site-cookbooks/mysql/templates/default/mysqld.conf.erb
[program:mysqld]
command=/usr/bin/mysqld_safe

おわりに

supervisorを使って複数のプロセスを起動させる場合、プロセスごとにDockerfileを用意する必要があり、効率が良くないなと考えていました。今回、Chefからコンテナの起動プロセスを追加することができたため、SSH接続ができるところまでをDockerfile、それ以降をChefといったプロビジョニングツールでといった役割分担ができそうです。

WindowsでSphinxからPDFファイルを作成する

WindowsSphinxからPDFファイルを作成しようとしたときに、なぜかWeb上に情報が少なく、はまってしまったのでメモしときます。

Sphinxのインストール

Pythonのインストール

SphinxPythonの2系と3系に対応しているようですが、2.7系が一番使われているようなので、2.7系をインストールします。
まず、Python.orgから「python-2.7.3.msi」をダウンロードします。ダウンロードができたら、「python-2.7.3.msi」をクリックして、pythonをインストールします。*1

インストール後に、システムの環境変数のPathに以下を追加します。

C:¥Python27
C:¥Python27¥Scripts

easy_installコマンドのインストール

https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.pyのリンクを右クリックからダウンロードして、「ez_setup.py」として保存します。
以下のコマンドを実行して、easy_installコマンドをインストールします。

> python ez_setup.py

Sphinxのインストール

以下のコマンドを実行して、Sphinxをインストールします。

> easy_install sphinx

PDFファイルの作成

TeXLiveのインストール

SphinxからPDFファイルを作成する場合、rst2pdfとLatex経由の2種類ありますが、Latex経由のほうが見た目がきれいなPDFファイルが作れるようなので、こちらの方法をとります。

Installing TeX Live over the Internetから「install-tl.zip」をダウンロードします。
「install-tl.zip」を展開し、「install-tl.bat」をクリックします。「install-tl.bat」をクリックすると、インストーラが起動します。
「既定リポジトリを変更」にチェックを入れ、「次へ」をクリックします。「ミラー」から日本のミラーサイトを選択します。後はインストーラに従って、インストールを行います。ファイルのダウンロードに時間がかかるので、時間に余裕のあるときにしたほうがいいかもしれません。

Sphinxプロジェクトの作成

Sphinxの初期設定として、プロジェクトを作ります。
プロジェクトを保存する任意のフォルダを作成し、以下のコマンドを実行します。

> mkdir <フォルダ名>
> cd <フォルダ名>
> sphinx-quickstart

基本的に設定が必要な箇所は以下の通りです。

Root path for the documentation [.]: フォルダに移動しているのでそのままリターン
Project name: <プロジェクト名>
Author name(s): <著者名>
Project version: <バージョン名>

Sphinxプロジェクトの設定変更

上記で作成したプロジェクトのフォルダ配下に「conf.py」というファイルがあるので、以下の内容をファイルに追加します。

# 言語の設定
language = 'ja'

# LaTeX の docclass 設定
latex_docclass = {'manual': 'jsbook'}

make.batの作成

この状態でmake latexpdfjpコマンドを実行しても、以下のようなメッセージが出力され、PDFファイルが作成されませんでした。

> make latexpdfja
Making output directory...
Running Sphinx v1.2.2
loading pickled environment... done
building [latex]: all documents
updating environment: 0 added, 0 changed, 0 removed
looking for now-outdated files... none found
processing BookReview.tex... index
resolving references...
writing... done
copying TeX support files...
done
build succeeded.
'make' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

Build finished; the PDF files are in _build/latex.

make.batの中を確認すると、make latexpdfjpコマンドが実行されると、_build\latexに移動し、make all-pdf-jaコマンドを実行していました。しかし、_build\latex配下にmake.batが存在しないため、実行に失敗していたようです。

Sphinx-Usersのメーリングリストを確認したところ、プロジェクト配下のmake.batファイルとは異なる「make.bat」を作成し、sphinx\texinputs配下に配置することで解決できそうです。

そこで、以下のmake.batを作成し、「C:\Python27\Lib\site-packages\sphinx-1.2.2-py2.7.egg\sphinx\texinputs」(デフォルトパス)に配置します。

> more make.bat
@@echo off
setlocal

::set %LATEXOPTS%=

if "%1" EQU "all-pdf" goto all-pdf
if "%1" EQU "all-dvi" goto all-dvi
if "%1" EQU "all-ps" goto all-ps
if "%1" EQU "all-pdf-ja" goto all-pdf-ja
if "%1" EQU "clean" goto clean

:all-pdf
for %%f in (*.tex) do pdflatex %LATEXOPTS% %%f
for %%f in (*.tex) do pdflatex %LATEXOPTS% %%f
for %%f in (*.tex) do pdflatex %LATEXOPTS% %%f
for %%f in (*.idx) do if exist %%f (makeindex -s python.ist %%f)
for %%f in (*.tex) do pdflatex %LATEXOPTS% %%f
for %%f in (*.tex) do pdflatex %LATEXOPTS% %%f
goto end

:all-dvi
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.idx) do if exist %%f (makeindex -s python.ist %%f)
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.tex) do latex %LATEXOPTS% %%f
goto end

:all-ps
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.idx) do if exist %%f (makeindex -s python.ist %%f)
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.tex) do latex %LATEXOPTS% %%f
for %%f in (*.dvi) do dvips %%f
goto end

:all-pdf-ja
for %%f in (*.pdf *.png *.gif *.jpg *.jpeg) do extractbb %%f
for %%f in (*.tex) do platex -kanji=utf8 %LATEXOPTS% %%f
for %%f in (*.tex) do platex -kanji=utf8 %LATEXOPTS% %%f
for %%f in (*.tex) do platex -kanji=utf8 %LATEXOPTS% %%f
for %%f in (*.idx) do if exist %%f (mendex -U -f -d "`basename %%f .idx`.dic" -s python.ist %%f)
for %%f in (*.tex) do platex -kanji=utf8 %LATEXOPTS% %%f
for %%f in (*.tex) do platex -kanji=utf8 %LATEXOPTS% %%f
for %%f in (*.dvi) do dvipdfmx %%f
goto end

:clean
del *.dvi *.log *.ind *.aux *.toc *.syn *.idx *.out *.ilg *.pla
goto end

:end

PDFファイルの作成

以下のコマンドを実行し、SphinxのドキュメントからLaTeX経由でPDFファイルを作成します。_build\latex配下にPDFファイルが作成されていることを確認します。

> make latexpdfja

*1:最新の2.7.6だとhtmlの生成に失敗する場合があったので、2.7.3にしています。

DockerでMySQLを起動するDockerfileを書いてみた

Dockerfileを書く練習のため、今回は、sshdとMySQLの複数プロセスを起動するDockerfileを書いてみました。Dockerはプロセスを起動させるコマンドを1つしか指定できないため、プロセス管理ツールのSupervisorを使用しています。また、コンテナ内のデータベースファイルをホスト側に保存されるようにしてみました。

構成

Ubuntu 12.04: サーバ構築対象
Ubuntu 12.04はDocker 0.10上で動作しています。

Dockerfileの作成

Dockerfileを以下のように作成します。

$ mkdir -p docker/mysql
$ cd docker/mysql
$ vi Dockerfile
FROM ubuntu:12.04

MAINTAINER hidemium

# Ubuntu update
ADD sources.list /etc/apt/sources.list
RUN apt-get -y update

# Hack for initctl not being available in Ubuntu
RUN dpkg-divert --local --rename --add /sbin/initctl
RUN ln -sf /bin/true /sbin/initctl

# ssh install
RUN apt-get -y install openssh-server
RUN apt-get -y install python-setuptools
RUN easy_install supervisor
RUN mkdir -p /var/log/supervisor

# MySQL install
RUN apt-get install -y mysql-server
RUN apt-get clean

RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd

# sshd config
ADD id_rsa.pub /root/id_rsa.pub
RUN mkdir /root/.ssh/
RUN mv /root/id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed -i -e '/^UsePAM\s\+yes/d' /etc/ssh/sshd_config

# supervisor config
RUN mkdir -p /var/log/supervisor
ADD supervisord.conf /etc/supervisord.conf

# MySQL config
ADD mysql-listen.cnf /etc/mysql/conf.d/mysql-listen.cnf
RUN (/usr/bin/mysqld_safe &); sleep 3; mysqladmin -u root password 'passw0rd'; (echo 'grant all privileges on *.* to root@"%" identified by "passw0rd" with grant option;' | mysql -u root -ppassw0rd)

# Define mountable directories.
VOLUME ["/var/lib/mysql"]

# Expose ports.
EXPOSE 22 3306

# Define default command.
CMD ["supervisord", "-n"]
  • 「RUN dpkg-divert --local --rename --add /sbin/initctl」と「RUN ln -sf /bin/true /sbin/initctl」は、DockerでMySQLを使うためのハックのようです。詳しくはまだ理解できていません。。
  • 「RUN easy_install supervisor」は、apt-getでsupervisorのインストールができなかったため、easy_installにてインストールを行っています。
  • 「ADD supervisord.conf /etc/supervisord.conf」は、supervisorから起動するプロセスの定義ファイルを追加しています。
  • 「ADD mysql-listen.cnf /etc/mysql/conf.d/mysql-listen.cnf」はホスト側からMySQLへ接続できるように許可します。
  • 「RUN (/usr/bin/mysqld_safe &)~」はMySQLを一度起動して、パスワードの設定とホスト側からMySQLへ接続できるように許可します。
  • 「VOLUME ["/var/lib/mysql"]」はホスト側でマウントするディレクトリを指定します。

Dockerfileの中で、読み込みを行っているファイルは以下の通りです。

$ vi sources.list
deb http://jp.archive.ubuntu.com/ubuntu precise main restricted
deb-src http://jp.archive.ubuntu.com/ubuntu precise main restricted
deb http://jp.archive.ubuntu.com/ubuntu precise-updates main restricted
deb-src http://jp.archive.ubuntu.com/ubuntu precise-updates main restricted
$ vi mysql-listen.cnf
[mysqld]
bind = 0.0.0.0
$ vi supervisord.conf
[supervisord]
nodaemon=true

[program:sshd]
command=/usr/sbin/sshd -D
autostart=true
autorestart=true

[program:mysqld]
command=/usr/bin/mysqld_safe
autostart=true
autorestart=true

Dockerの場合、MySQLの起動にservice mysql startが使えないため、/usr/bin/mysqld_safeで起動します。

MySQLコンテナの起動

以下のコマンドで、Dockerfileからビルドし、Dockerコンテナの起動を行います。
Dockerコンテナの起動時には、-vオプションを指定し、コンテナ内のデータベースファイルをホストのディレクトリに保存するようにしています。

$ sudo docker build -t ubuntu-mysql .
$ sudo docker run -d -p 22 -p 3306 -v /opt/mysql/data:/var/lib/mysql --name ubuntu-mysql ubuntu-mysql

mysqld_safeで起動しているため、ビルドするときに以下のエラーが出ることがあります。

140522 18:26:08 mysqld_safe Can't log to error log and syslog at the same time.  Remove all --log-error configuration options for --syslog to take effect.
140522 18:26:08 mysqld_safe Logging to '/var/log/mysql/error.log'.
140522 18:26:08 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql

Dockerコンテナの3306番ポートにフォワードしているポート番号を確認し、ホスト側からMySQLに接続できることを確認します。
Dockerfileで外部から接続する許可を行っているため、コンテナの起動直後からMySQLにログインできます。

$ sudo docker port ubuntu-mysql 3306
0.0.0.0:<ポート番号>
$ mysql -uroot -h 127.0.0.1 -P <ポート番号> -ppassw0rd

rbenvでruby をインストールするChefのレシピを書いてみた

前回は、Opscode Communityに公開さているレシピを使い、rubyをインストールしましたが、Chefのレシピを書く練習のため、今回は、rbenvでrubyをインストールするレシピを書いてみました。

構成

Windows 7: Chef 11.10
Ubuntu 12.04: サーバ構築対象
Ubuntu 12.04はDocker 0.10上で動作しています。

レシピの作成

Rubyインストール用のcookbookを作成します。

$ cd chef-repo/
$ knife cookbook create ruby -o site-cookbooks

recipesファイルを以下のように編集します。
設定内容は以前の記事とほぼ同じですが、「.bash_profile」を「rbenv.sh」に変更したり、Ubuntuに合わせて導入パッケージを変更したりしています。また、実行ユーザはrootユーザを想定しています。

$ vi site-cookbooks/ruby/recipes/default.rb
%w{git-core build-essential libssl-dev}.each do | pkg |
    package pkg do
        action :install
    end
end

git "/usr/local/rbenv" do
    repository "git://github.com/sstephenson/rbenv.git"
    reference "master"
    action :sync
end

%w{/usr/local/rbenv/shims /usr/local/rbenv/versions}.each do |dir|
    directory dir do
    action :create
    end
end

git "/usr/local/ruby-build" do
    repository "git://github.com/sstephenson/ruby-build.git"
    reference "master"
    action :sync
end

bash "install_ruby_build" do
    cwd  "/usr/local/ruby-build"
    code "./install.sh"
    action :run
end

template "rbenv.sh" do
    path "/etc/profile.d/rbenv.sh"
    owner "root"
    group "root"
    mode "0644"
    source "rbenv.sh.erb"
end

bash "rbenv install" do
    code   "source /etc/profile.d/rbenv.sh; rbenv install #{node.build}"
    action :run
    not_if { ::File.exists?("/usr/local/rbenv/versions/#{node.build}") }
end

bash "rbenv rehash" do
    code   "source /etc/profile.d/rbenv.sh; rbenv rehash"
    action :run
end

bash "rbenv global" do
    code   "source /etc/profile.d/rbenv.sh; rbenv global #{node.build}"
    action :run
end

attributesファイルを以下のように編集します。
インストールするRubyのバージョンを変更する場合は、attributesファイルで変更します。※2.0.0-p353も対応しています。

$ vi site-cookbooks/ruby/attributes/default.rb
default["build"] = "1.9.3-p545"

templatesファイルを以下のように編集します。

$ vi site-cookbooks/ruby/templates/default/rbenv.sh.erb
export RBENV_ROOT="/usr/local/rbenv"
export PATH="/usr/local/rbenv/bin:$PATH"
eval "$(rbenv init -)"

JSONファイルにRubyインストール用のcookbookを指定します。

$ vi nodes/<サーバのIPアドレス>.json
{
  "run_list":[
    "recipe[ruby]"
  ]
}

サーバにChefをインストールします。

$ knife solo bootstrap root@<サーバのIPアドレス> -p <ポート番号>

他のOSバージョンでの実行

今回、Ubuntu 12.04に対するレシピを作成しましたが、それ以外のOSバージョンで上記のレシピを実行した場合にどうなるか試してみました。
レシピを実行した結果、以下のようなエラーがでました。

---- Begin output of apt-get -q -y install git-core=1:1.7.9.5-1 ----
STDOUT: Reading package lists...
Building dependency tree...
Reading state information...
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:

The following packages have unmet dependencies:
 git-core : Depends: git (> 1:1.7.0.2) but it is not going to be installed
STDERR: E: Unable to correct problems, you have held broken packages.
---- End output of apt-get -q -y install git-core=1:1.7.9.5-1 ----
Ran apt-get -q -y install git-core=1:1.7.9.5-1 returned 100
WARNING: Chef-Client has not been regression tested on this O/S Distribution
WARNING: Do not use this configuration for Production Applications.  Use at your own risk.
:
---- Begin output of apt-get -q -y install build-essential=11.5ubuntu2.1 ----
STDOUT: Reading package lists...
Building dependency tree...
Reading state information...
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:

The following packages have unmet dependencies:
 build-essential : Depends: libc6-dev but it is not going to be installed or
                            libc-dev
                   Depends: g++ (>= 4:4.4.3) but it is not going to be installed

STDERR: E: Unable to correct problems, you have held broken packages.
---- End output of apt-get -q -y install build-essential=11.5ubuntu2.1 ----
Ran apt-get -q -y install build-essential=11.5ubuntu2.1 returned 100

予想以上にOSのバージョン差異による影響がありました。また、対象OSでChef-Clientがテストされていない場合もあるようです。Chefを導入する場合は、テストが重要になってきそうです。
ちなみに、他のOSバージョンでのテストも、Dockerを用いて行いました。テストごとに環境を使い捨てにできるで、さくっと使えて便利ですね。

WindowsへのChef Soloのインストールと環境改善

WindowsにChefをインストールできるようなので試してみました。また、Chefの実行はコマンドプロンプトから行うため、合わせてコマンドプロンプトを使いやすくしてみた。

構成

Windows 7: Chef 11.10
Ubuntu 12.04: サーバ構築対象
Ubuntu 12.04はDocker 0.10上で動作しています。

インストール

Ruby

まず、WindowsRubyのインストールを行います。
Windows用のインストーラーが用意されているため、RubyInstallerから「rubyinstaller-1.9.3-p545.exe」をダウンロードします。*1

「rubyinstaller-1.9.3-p545.exe」をクリックし、Rubyをインストールします。
ほぼデフォルトのままで良いかと思いますが、「インストール先とオプションの指定」では、以下のように指定しました。

  • インストールフォルダ: C:\Ruby\1.9.3-p545
  • Rubyの実行ファイルへ環境変数PATHを設定する チェック

インストール後に、コマンドプロンプトにて以下のコマンドを実行し、Rubyがインストールされていることを確認します。

> ruby -v
ruby 1.9.3p545 (2014-02-24) [i386-mingw32]

DevKit

ChefのインストールにDevKitも必要であるため、同じサイトから「DevKit-tdm-32-4.5.2-20111229-1559-sfx.exe」をダウンロードします。

適当なフォルダに展開し、以下のコマンドを実行し、インストールを行います。

> cd C:\Ruby\DevKit
> ruby dk.rb init
> ruby dk.rb install

cwRsync

ChefはSSHを使用しているため、WindowsでもSSHを使用できるようにするため、cwRsyncというソフトウェアを使用します。
cwRsync - Rsync for Windowsから「Free」をクリックします。次にダウンロードページから「Download!」をクリックし、「cwRsync_5.3.0_Free.zip」をダウンロードします。

適当なフォルダに展開します。

> mkdir C:\User\<アカウント名>\bin
> C:\Users\<アカウント名>\bin\cwRsync_5.3.0に展開

次に、システム環境変数のPathに以下を追加します。

;C:\Users\<アカウント名>\bin\cwRsync_5.3.0

以下のコマンドを実行し、他サーバへSSH接続が可能か確認します。

> ssh <ユーザ名>@<サーバのIPアドレス>:

サーバにパスワードなしで接続できるように、公開鍵認証の設定を行います。
ssh-keygenコマンドで認証用の鍵を作成し、対象サーバへ転送します。

> ssh-keygen
> cd C:\User\<アカウント名>\.ssh
> rsync id_rsa.pub <ユーザ名>@<サーバのIPアドレス>:
> ssh <ユーザ名>@<サーバのIPアドレス> # サーバへログイン
$ cat id_rsa.pub >> ~/.ssh/authorized_keys

Chef

以下のコマンドを実行し、Chefのインストールを行います。
このあたりはCentOSと変わりません。

> cd C:\User\<アカウント名>
> gem install chef -v 11.10 # Chefをインストール
> knife configure # 初期設定
> gem install knife-solo

今回、Ruby1.9系を使用しましたが、Ruby2.0系でknife configureコマンドを実行した際に、以下のメッセージが出力される場合があり、おとなしくRuby1.9系にしました。

> knife configure
ERROR: Ohai::Exceptions::DependencyNotFound: Can not find a plugin for dependency os
> knife configure
DL is deprecated, please use Fiddle
C:/Ruby/2.0.0-p481/lib/ruby/gems/gems/windows-api-0.4.0/lib/windows/api.rb:1:in `req
uire': 126: 指定されたモジュールが見つかりません。 - C:/Ruby/2.0.0-p481/lib/ruby/g
ems/gems/win32-api-1.4.6-x86-mswin32-60/lib/win32/api.so (LoadError)

また、Chef11.12系では、knife cookをした際に以下のような警告メッセージが出力されるようになりました。Windowsでの対処方法が見つからなかったため、Chef11.10系を指定してインストールしています。

SSL validation of HTTPS requests is disabled. HTTPS connections are still                       
encrypted, but chef is not able to detect forged replies or man in the middle                   
attacks.                                                                                        
                                                                                                
To fix this issue add an entry like this to your configuration file:                            
                                                                                                
```                                                                                             
  # Verify all HTTPS connections (recommended)                                                  
  ssl_verify_mode :verify_peer                                                                  
                                                                                                
  # OR, Verify only connections to chef-server                                                  
  verify_api_cert true                                                                          
```                                                                                             
                                                                                                
To check your SSL configuration, or troubleshoot errors, you can use the                        
`knife ssl check` command like so:                                                              
                                                                                                
```                                                                                             
  knife ssl check -c /tmp/vagrant-chef-1/solo.rb                                                
```                                                   

次に、Chefのリポジトリを作成します。

> knife solo init chef-repo

cookbookを作成します。

> cd chef-repo
> knife cookbook create apache2 -o site-cookbooks
> site-cookbooks/apache2/recipes/default.rb
package "apache2" do
    action :install
end

サーバのIPアドレス.jsonのファイルが作成されるため、実行したいcookbookを指定します。

> nodes/<サーバのIPアドレス>.json
{
  "run_list":[
    "recipe[apache2]"
  ]
}

サーバへChefをインストールし、cookbookを実行します。

> knife solo bootstrap <ユーザ名>@<サーバのIPアドレス> -p <Dockerコンテナのポート番号>

コマンドプロンプトを使いやすくする

Console

コマンドプロンプトを拡張する「Console」というソフトウェアがあります。このソフトウェアは、コマンドラインの実行環境はそのままで、フォントの変更やコピーアンドペーストの挙動、背景の透明化、タブ化など、コマンドプロンプトではできない機能を使用することができます。

Consoleから、「Console-2.00b148-Beta_32bit.zip」をダウンロードし、適当なフォルダに展開します。

> mkdir C:\User\<アカウント名>\tools
> C:\Users\<アカウント名>\tools\Consoleに展開

Console.exeから起動することができます。
[Edit]>[Setting..]をクリックすると、Console Settingの画面が開きます。
代表的な設定は以下で設定することができます。

  • フォント: Appearance>Font>Name
  • コピーアンドペーストの挙動: Behavior>Copy Paste
  • 背景の透明化: Appearance>More..>Window transparency>Alpha>Active window,Inactive windows
  • 上矢印キーで履歴: Hotkeys>Use Scroll Lock for scrollingをOFF

Vim

コマンドプロンプトにはCUIエディタがないため、Windows用のVimをインストールします。
Vim — KaoriYaから「vim74-kaoriya-win32-20140504.zip」をダウンロードし、適当なフォルダに展開します。

> C:\Users\<アカウント名>\bin\vim74に展開

次に、システム環境変数のPathに以下を追加します。

;C:\Users\<アカウント名>\bin\vim74

なお、GUIのエディタでレシピを編集しても実行可能なため、必ずしもVimにする必要はありません。(好みの問題です。)

doskey

Windowsにもdoskeyというエイリアス機能があるので、Linuxライクなコマンドが使えるように変更します。

ホームディレクトリにcmd.aliasesというファイルを作成し、以下のような設定を入力します。

> %HOME%\cmd.aliases
pwd=cd
mv=move $*
rm=del $*
grep=find "$1" $2
diff=fc $*
cp=copy $*
cat=type $*
ls=dir /b $*
ll=dir $*
vi=vim $*
ps=tasklist $*

Consoleの[Edit]>[Setting..]をクリックすると、Console Settingにて以下の設定を行います。

  • シェル: Console>Shell
C:\Windows\system32\cmd.exe /K doskey /macrofile=%HOME%\cmd.aliases

例えば、「ll」と入力すると、dirコマンドの結果を見ることができます。

> ll
2014/05/17  02:59    <DIR>          .
2014/05/17  02:59    <DIR>          ..
2014/05/16  14:24    <DIR>          .chef
2014/05/16  14:24                13 .gitignore
2014/05/17  00:07                87 chef_run.bat
2014/05/16  14:24    <DIR>          cookbooks
:

これで、Windowsから直接Chefを実行する環境が整いました。

*1:2014/5時点で最新のものです。

DockerでアプリケーションとMySQLをコンテナ化してRedmineを構築する

Dockerの公式リポジトリsameersbn/redmineというRedmineのイメージがあったので試してみました。

このイメージの面白いところは、アプリケーションだけでなくデータベースもコンテナにし、さらにデータファイルをコンテナから分離してるところです。

構成

Ubuntu 12.04: Docker 0.10.0、Redmine 2.5.1、MySQL 5.6
※上記のサーバはVMware ESXi 5.1上で動作しています。

インストール

MySQL

まずは、以下のコマンドでMySQLのイメージをダウンロードします。

$ sudo docker pull sameersbn/mysql:latest

次にMySQLのデータディレクトリをホスト側で保存するため、ホスト側でディレクトリを作成します。

$ sudo mkdir -p /opt/mysql/data

docker runコマンドでMySQLのコンテナを起動します。
「-v」オプションを使い、コンテナ内のファイルを上記で作成したディレクトリに保存するようにしています。

$ sudo docker run --name mysql -d -v /opt/mysql/data:/var/lib/mysql sameersbn/mysql:latest

以下のコマンドで、MySQLのコンテナにログインします。
「docker inspect mysql~」は、MySQLのコンテナのIPアドレスを取得しています。

$ sudo mysql -uroot -h $(sudo docker inspect mysql | grep IPAddres | awk -F'"' '{print $4}')

MySQLのコンテナにログイン後、「redmine」ユーザと「redmine_production」データベースを作成します。
「172.17.%.%」と指定することで、コンテナ間での接続ができるようにしています。

CREATE USER 'redmine'@'172.17.%.%' IDENTIFIED BY 'redmine';
CREATE DATABASE IF NOT EXISTS `redmine_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `redmine_production`.* TO 'redmine'@'172.17.%.%';
FLUSH PRIVILEGES;

Redmine

以下のコマンドでRedmineのイメージをダウンロードします。

$ sudo docker pull sameersbn/redmine:latest

次にRedmineに登録したファイルをホスト側で保存するため、ホスト側でディレクトリを作成します。

$ sudo mkdir -pv /opt/redmine/files

以下のコマンドでRedmineの初期設定とデータベースのテーブルを作成します。
「--rm」オプションはコンテナ起動後にコンテナを削除する設定です。
「--link」オプションはコンテナ間の連携をする設定で、「連携したいコンテナ名:エイリアス名」を指定します。
「-e」オプションは環境変数を指定するための設定です。
「-v」オプションはコンテナ内のファイルをホスト側に保存するための設定です。

$ sudo docker run --name redmine2.5.1_link -i -t --rm --link mysql:mysql -e "DB_USER=redmine" -e "DB_PASS=redmine" -e "DB_NAME=redmine_production" -v /opt/redmine/files:/redmine/files sameersbn/redmine:latest app:db:migrate
User: root Password: xxxxxxxxx
sshd: started
memcached: stopped
memcached: updated process group
memcached: started
Your Gemfile lists the gem fastercsv (~> 1.5.0) more than once.
You should probably keep only one of them.
While it's not a problem now, it could cause errors if you change the version of just one of them later.
Your Gemfile lists the gem yard (>= 0) more than once.
You should probably keep only one of them.
While it's not a problem now, it could cause errors if you change the version of just one of them later.
redmine_production already exists
Your Gemfile lists the gem fastercsv (~> 1.5.0) more than once.
You should probably keep only one of them.
While it's not a problem now, it could cause errors if you change the version of just one of them later.
Your Gemfile lists the gem yard (>= 0) more than once.
You should probably keep only one of them.
While it's not a problem now, it could cause errors if you change the version of just one of them later.
==  Setup: migrating ==========================================================
-- create_table("attachments", {:force=>true})
   -> 0.0240s
-- create_table("auth_sources", {:force=>true})
   -> 0.0076s
-- create_table("custom_fields", {:force=>true})
   -> 0.0074s
-- create_table("custom_fields_projects", {:id=>false, :force=>true})
:

以下のコマンドで、Redmineを起動します。

$ sudo docker run --name redmine2.5.1_link -d -p 80 --link mysql:mysql -e "DB_USER=redmine" -e "DB_PASS=redmine" -e "DB_NAME=redmine_production" -v /opt/redmine/files:/redmine/files sameersbn/redmine:latest

コンテナの状態を確認すると、RedmineMySQLのコンテナが起動してるのが分かります。
80番ポートにポートフォワードするポートは、PORTSの「0.0.0.0:49221->80/tcp 」から「49221」であることが分かります。

$ sudo docker ps -a
CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS                    PORTS                                          NAMES
f46be945ba2d        sameersbn/redmine:latest   /redmine/init app:st   5 hours ago         Up 5 hours                22/tcp, 0.0.0.0:49221->80/tcp                  redmine2.5.1_link
b9f0ba33ab50        sameersbn/mysql:latest     /app/init app:start    5 hours ago         Up 5 hours                22/tcp, 3306/tcp                               mysql,redmine2.5.1_link/mysql

ブラウザから、「http://localhost:49221」にアクセスし、Redmineのログイン画面が表示されることを確認します。
ただし、アクセス直後はInternal Error 500というエラーが出る場合があり、何度かリロードすると正しく表示されるようになりました。

おわりに

Immutable Infrastructureの実現には、Stateless ServerはDockerを利用し、Stateful Serverは今まで通りDBサーバを利用して運用するイメージを持っていました。
しかし、Dockerはファイルを分離してホスト側で保持することができ、DBサーバもコンテナで起動することができるため、Dockerの利用用途としてStateful Serverも対象になる可能性があると感じました。
もし、DBサーバもコンテナとして利用できれば、Blue-Green Deploymentといった手法として、以下のようにDBサーバごと切り替えられるかもしれません。
※DBサーバを物理的にもう一台用意するイメージかもしれませんが。。

・Blue: APコンテナ(現行) - DBコンテナ(現行)
・Green: APコンテナ(新規) - DBコンテナ(新規)

今後、DBサーバをコンテナ化した場合に、どのような問題が発生するか試してみたいと思います。(あまり情報がないため、知っている人がいればぜひ教えてください。)

Docker + Chef + serverspec + Jenkins でインフラCIの環境を構築してみた

Dockerが使えるようになったため、Jenkinsにより仮想サーバの起動から、サーバ構築、テスト、仮想サーバの廃棄までを自動化してみました。

やりたいこと

以下のように、Chefのリポジトリの更新をトリガーに、仮想サーバの起動から、サーバ構築、テスト、仮想サーバの廃棄までをJenkinsにて自動化します。

  1. Chefのレシピをリモートリポジトリへgit pushすると、Jenkinsが通知を検知
  2. JenkinsからDockerの仮想サーバ(コンテナ)を起動
  3. 起動が成功すれば、Chefを実行し、サーバを構築
  4. サーバ構築が成功すれば、serverspecを実行し、サーバの状態をテスト
  5. テストが成功すれば、Dockerの仮想サーバ(コンテナ)を廃棄

また、Dockerの起動停止、サーバ構築、テストは全てSSH接続により行います。

構成

CentOS 6.5 : Chef、serverspec、Jenkins、Gitをインストール
Ubuntu 12.04 : Dockerをインストール、サーバ構築、テスト対象
※上記2台のサーバはVMware ESXi 5.1上で動作しています。

インストール

以下のソフトウェアをインストールします。
※過去の記事のリンクになります。

設定

各ソフトウェアの設定について説明していきます。

Docker

まず、DockerでSSH接続が可能なイメージを作成します。
今回、Dockerのイメージを作成するために、コンテナでの処理を記述することができるDockerfileを使用しました。
このDockerfileで、CentOSからSSH接続を行うために、CentOS側の公開鍵の設定も行っています。

$ cd docker #作業ディレクトリに移動します。
$ cp ~/id_rsa.pub . #CentOS側の公開鍵を作業ディレクトリにコピーします。
$ vi Dockfile
FROM ubuntu:12.04
MAINTAINER hidemium

RUN apt-get -y update

# install
RUN apt-get -y install openssh-server

RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd

# sshd config
ADD id_rsa.pub /root/id_rsa.pub #公開鍵をコンテナに追加します。
RUN mkdir /root/.ssh/
RUN mv /root/id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed -i -e '/^UsePAM\s\+yes/d' /etc/ssh/sshd_config

EXPOSE 22
CMD /usr/sbin/sshd -D

docker buildコマンドにより、Dockerfileに記述された処理を実行後、commitをしてイメージの作成までを自動で行います。

  • tオプションにより、イメージ名を指定します。

実行後、ubuntu-sshdというイメージが作成されていることを確認します。

$ sudo docker build -t ubuntu-sshd .
$ sudo docker images

CentOS側からSSH接続が可能になったか確認するため、テストします。
先ほど作成したイメージから、22番ポートのポートフォワードを指定し、コンテナを起動します。
コンテナ起動後に、PORTSの列に「0.0.0.0:[ポート番号]」から、22番ポートにマッピングされたローカルのポートを確認します。

$ sudo docker run -d -p 22 ubuntu-sshd
$ sudo docker ps -a
CONTAINER ID        IMAGE                COMMAND                CREATED             STATUS                         PORTS                                  NAMES
6e61ccbe71df        ubuntu-sshd:latest   /bin/sh -c '/usr/sbi   2 days ago          Up 25 hours                    0.0.0.0:[ポート番号]->22/tcp                         loving_almeida

CentOS側から、以下のコマンドを実行しログインできることを確認します。

$ ssh root@<Ubuntu側のIPアドレス> -p [ポート番号]

Chef

Chefのリポジトリを作成します。

$ knife solo init chef-repo

サーバ構築用のcookbookを作成します。
今回は、apache2をインストールするレシピを用意します。

$ cd chef-repo/
$ knife cookbook create apache2 -o site-cookbooks
$ vi site-cookbooks/apache2/recipes/default.rb
# Apacheをインストール
# sudo apt-get -y install apache2
package "apache2" do
    action :install
end

JSONファイルが作成されるため、実行するapache2のcookbookを指定します。

$ vi nodes/<サーバのIPアドレス>.json
{
  "run_list":[
    "recipe[apache2]"
  ]
}

serverspec

Chefのrootディレクトリに移動し、serverspecの初期設定を行います。

$ cd chef-repo/
$ serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1  #UN*Xを選択

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 1 #SSHを選択

Vagrant instance y/n: n #nを選択
Input target host name: 192.168.xxx.xxx #テスト対象サーバのIPアドレスを指定
 + spec/
 + spec/192.168.xxx.xxx/
 + spec/192.168.xxx.xxx/httpd_spec.rb
 + spec/spec_helper.rb
 + Rakefile

apache2がインストールされているか確認するテストコードを書きます。

$ mv spec/192.168.xxx.xxx/httpd_spec.rb spec/192.168.xxx.xxx/apache2_spec.rb
$ vi spec/192.168.xxx.xxx/apache2_spec.rb
require 'spec_helper'

describe package('apache2') do
  it { should be_installed }
end

serverspecは、SSH接続でテスト対象にアクセスしますが、テスト対象にログインするための情報は~/.ssh/configから取得しています。
serverspecが利用できるように、~/.ssh/configを修正します。

$ vi ~/.ssh/config
Host 192.168.xxx.xxx
  HostName 192.168.xxx.xxx #テスト対象サーバのIPアドレスを指定
  User root #SSH接続でログインするユーザを指定
  Port 22
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile "/root/.ssh/id_rsa" #秘密鍵のパスを指定
  IdentitiesOnly yes
  LogLevel FATAL

Dockerは、新たなコンテナを起動するたびに、22番ポートにフォワードされるポートが毎回異なって生成される問題があります。
Chefの場合は、-pオプションを使用すれば、ポート番号を指定できますが、serverspecは~/.ssh/configからポート番号を取得しており、ポート番号を引数で与えることができません。
今回、~/.ssh/configからSSH接続の情報を取得している、spec/spec_helper.rbを修正し、外部ファイルからポート番号を設定できるようにしました。

$ vi spec/spec_helper.rb
require 'serverspec'
require 'pathname'
require 'net/ssh'

include SpecInfra::Helper::Ssh
include SpecInfra::Helper::DetectOS

RSpec.configure do |c|
  if ENV['ASK_SUDO_PASSWORD']
    require 'highline/import'
    c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
  else
    c.sudo_password = ENV['SUDO_PASSWORD']
  end
  c.before :all do
    block = self.class.metadata[:example_group_block]
    if RUBY_VERSION.start_with?('1.8')
      file = block.to_s.match(/.*@(.*):[0-9]+>/)[1]
    else
      file = block.source_location.first
    end
    host  = File.basename(Pathname.new(file).dirname)

+   SSH_CONFIG_FILE = 'spec/.ssh-config'
+   config = File.open(SSH_CONFIG_FILE).read
+   port = ""
+   if config != ''
+     config.each_line do |line|
+       if match = /Port (.*)/.match(line)
+         port = match[1]
+       end
+     end
+   end

    if c.host != host
      c.ssh.close if c.ssh
      c.host  = host
      options = Net::SSH::Config.for(c.host)
      user    = options[:user] || Etc.getlogin
+     options[:port] = port
      c.ssh   = Net::SSH.start(host, user, options)
    end
  end
end

ファイルは、spec/.ssh-configから取得しており、ポート番号が12345だった場合、以下のような内容になります。

$ echo Prot 12345 > spec/.ssh-config
$ cat spec/.ssh-config
Prot 12345

Git

Chef・serverspec用のリモートリポジトリを作成します。

$ mkdir /var/lib/git
$ mkdir /var/lib/git/chef-cookbooks.git
$ cd /var/lib/git/chef-cookbooks.git
$ git --bare init

Chef・serverspec用のローカルリポジトリを作成します。
chef-repoのディレクトリに移動し、リポジトリの初期化を行い、ローカルリポジトリを作成します。
リモートリポジトリには何も入っていないため、ローカルリポジトリの内容を追加します

$ cd chef-repo/
$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin /var/lib/git/chef-cookbooks.git
$ git push origin master

Jenkins

Jenkinsでのジョブは、jenkinsユーザにより実行されるため、パスワードなしでsudoを実行する設定を入れておきます。

$ visudo
jenkins ALL=(ALL) NOPASSWD: ALL # ←最終行に追加

Jenkinsにログイン後、[新規ジョブの作成]をクリックし、ジョブ名とビルドを設定します。

以下のように設定を入れ、[OK]をクリックします。

  • ジョブ名: infrastructure-ci
  • フリースタイル・プロジェクトのビルド オン

上記で作成したリポジトリと連携するため、[ソースコード管理]を以下のように設定します。

  • ソースコード管理
    • Git オン
    • Repositories
      • Repository URL: /var/lib/git/chef-cookbooks.git
      • Credentials: なし
    • Branches to build
      • Branch Specifier (blank for 'any'): */master

リポジトリのgit pushを検知するために、[ビルド・トリガ]を以下のように設定します。

  • ビルド・トリガ
    • SCMをポーリング チェック
      • スケジュール 下記に記載
      • post-commitフックを無視 チェックオフ

スケジュールはcronのように設定できます。
今回は、15分毎にチェックが走るように、以下のように設定します。

H/15 * * * *

Chefとserverspecのコマンドを実行するために、[ビルド手順を追加]>[シェルの実行]をクリックし、[シェルの実行]を以下のように設定します。

container_id=`sudo ssh chef@192.168.xxx.xxx sudo docker run -d -p 22 ubuntu-sshd`
container_port=`sudo ssh chef@192.168.xxx.xxx sudo docker inspect --format="'{{(index (index .NetworkSettings.Ports \"22/tcp\") 0).HostPort}}'" $container_id`
sudo /root/.rbenv/shims/knife solo bootstrap root@192.168.xxx.xxx -p $container_port
echo Port $container_port > spec/.ssh-config
sudo /root/.rbenv/shims/rake ci:setup:rspec spec
sudo ssh chef@192.168.xxx.xxx sudo docker stop $container_id
sudo ssh chef@192.168.xxx.xxx sudo docker rm $container_id
  • ビルド後の処理
    • JUnitテスト結果の集計
      • テスト結果XML
spec/reports/*.xml

上記の設定ができたら、[保存]をクリックします。

シェルスクリプトの説明

上記のシェルスクリプトの内容は、今回の環境構築でメインとなるところです。
各コマンドについて説明していきます。

1行目は、docker runで起動すると、コンテナIDが出力されるため、$container_idに格納しています。

すでにserverspecのところで説明しましたが、Dockerを使った構成で問題となるのは、22番ポートをフォワードしているポート番号の取得にあります。
Dockerのコンテナから情報を取得するには、Remote APIを使った方法やdocker-clientを使う方法がありましたが、シンプルにしたかったので、 docker inspectコマンドを使いました。

以下のコマンドで、22番ポートをフォワードしているポート番号を取得することができます。

docker inspect --format='{{(index (index .NetworkSettings.Ports "22/tcp") 0).HostPort}}' <コンテナID>

2行目は、コンテナIDからポート番号を取得し、$container_portに格納しています。
3行目は、knife solo bootstrapコマンドにより、prepareとcookを同時に実行しています。また、-pオプションによりコンテナのポート番号を指定しています。
4行目は、コンテナのポート番号をspec/.ssh-configに出力しています。
5行目は、serverspecを実行していますが、内部でspec/spec_helper.rbが呼ばれており、spec/.ssh-configの値を取得しています。このポート番号利用してSSH接続が行われます。
また、テスト結果をレポートするため、ci_reporterを使用しています。
ci_reporterの使用には、事前に以下の準備をしておきます。

$ gem install ci_reporter
$ vi Rakefile
require 'ci/reporter/rake/rspec'

6行目は、docker stopコマンドにより、コンテナの停止を行っています。docker rmコマンドでいきなり削除してもよかったのですが、時々削除に失敗して異常終了することがあったためです。他サイトの情報を確認したところ、一度停止するほうがいいようです。
7行目は、docker rmコマンドにより、コンテナの削除を行っています。

実行

以下のように、Chefのファイルを修正後、commitし、git pushを実行します。

$ cd chef-repo/
$ vi site-cookbooks/apache2/recipes/default.rb
$ git add .
$ git commit -m "first commit"
$ git push origin master

スケジュールのタイミングによりgit pushの通知が検知されます。
通知を検知すると、JenkinsのワークスペースにChefのリポジトリがコピーされます。

ジョブが実行されたら、[ジョブ名]>[ビルド履歴]>[ビルド番号]へ移動し、[コンソール出力]をクリックします。ジョブが正常終了すれば、コンソール出力の末尾に「Finished: SUCCESS」と表示されます。

以下は、Dockerの起動、Chefとserverspecの実行、Dockerの削除が正常終了していることを示しています。
ちなみに、Chefのレシピはapache2のインストールしか書いていませんが、仮想サーバの起動から廃棄まで2分程度で終了していました。

SCMのポーリングが実行
ビルドします。 ワークスペース: /var/lib/jenkins/workspace/infrastructure-ci
Fetching changes from the remote Git repository
Fetching upstream changes from /var/lib/git/chef-cookbooks.git
Checking out Revision 187f8b995de5ddc053a18a3d114f1aa5018f1979 (origin/master)
[infrastructure-ci] $ /bin/sh -xe /tmp/hudson7478251361203469042.sh
++ sudo ssh chef@192.168.xxx.xxx sudo docker run -d -p 22 ubuntu-sshd
+ container_id=e0a57b71d3e7352f7b405c5f9f74f746f3b3611107ea5766d894d319884e1998
++ sudo ssh chef@192.168.xxx.xxx sudo docker inspect '--format='\''{{(index (index .NetworkSettings.Ports "22/tcp") 0).HostPort}}'\''' e0a57b71d3e7352f7b405c5f9f74f746f3b3611107ea5766d894d319884e1998
+ container_port=49164
+ sudo /root/.rbenv/shims/knife solo bootstrap root@192.168.xxx.xxx -p 49164
Bootstrapping Chef...
:
hef Client finished, 1/1 resources updated in 44.868323127 secono Port 49164
+ sudo /root/.rbenv/shims/rake ci:setup:rspec spec
rm -rf spec/reports
/root/.rbenv/versions/2.0.0-p451/bin/ruby -S rspec spec/192.168.xxx.xxxx/apache2_spec.rb
.

Finished in 0.10092 seconds
1 example, 0 failures
+ sudo ssh chef@192.168.xxx.xxx sudo docker stop e0a57b71d3e7352f7b405c5f9f74f746f3b3611107ea5766d894d319884e1998
e0a57b71d3e7352f7b405c5f9f74f746f3b3611107ea5766d894d319884e1998
+ sudo ssh chef@192.168.xxx.xxx sudo docker rm e0a57b71d3e7352f7b405c5f9f74f746f3b3611107ea5766d894d319884e1998
e0a57b71d3e7352f7b405c5f9f74f746f3b3611107ea5766d894d319884e1998
Recording test results
Finished: SUCCESS

実行が失敗した場合は、以下のようにエラーが表示され、そこでJenkinsのジョブは停止します。

Build step 'シェルの実行' marked build as failure
Finished: FAILURE

ジョブの途中で失敗した場合は、後続の処理が実行されないため、正常終了すれば仮想サーバの起動から、サーバ構築、テスト、仮想サーバの廃棄まですべて終了したことになります。

おわりに

今回、インフラのCIを実現するうえで必要な仮想サーバの起動から、サーバ構築、テスト、仮想サーバの廃棄までの処理を全て自動化することができました。また、Dockerを導入したことで、全ての処理が完了するまで2分程度で終了したため、テスト回数が増加しても対応できるレベルとなりました。この処理時間の短さは、インフラのICを実現するのに欠かせないものであると考えています。この仕組みが実際に使われれば、常に動くChefのレシピがある状態を保てるかと思います。
また、仮想サーバにAWSを使ったり、GitをGitHubなどのホスティングサービスを使うなど、細かな点で変更は可能ですが、全体の流れとしてある程度完成した形になったと思います。後は、実際に運用を行ってみて、ルール決めが必要となったり、欠点などが見えてくるのではと思っています。*1
今回の記事がみなさんのお役に立てられればと思います。

*1:一応見えている問題としては、ジョブに失敗すると、Dockerのコンテナが削除されずに残ってしまうため、どういうタイミングで消すかというものがあります。