Dockerが使えるようになったため、Jenkinsにより仮想サーバの起動から、サーバ構築、テスト、仮想サーバの廃棄までを自動化してみました。
やりたいこと
以下のように、Chefのリポジトリの更新をトリガーに、仮想サーバの起動から、サーバ構築、テスト、仮想サーバの廃棄までをJenkinsにて自動化します。
- Chefのレシピをリモートリポジトリへgit pushすると、Jenkinsが通知を検知
- JenkinsからDockerの仮想サーバ(コンテナ)を起動
- 起動が成功すれば、Chefを実行し、サーバを構築
- サーバ構築が成功すれば、serverspecを実行し、サーバの状態をテスト
- テストが成功すれば、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フックを無視 チェックオフ
- SCMをポーリング チェック
スケジュールは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
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のコンテナが削除されずに残ってしまうため、どういうタイミングで消すかというものがあります。