hidemium's blog

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

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のコンテナが削除されずに残ってしまうため、どういうタイミングで消すかというものがあります。

Dockerのインストールと動作確認

VMwareVirtualBoxなどのハイパーバイザ型とは異なり、起動が早く軽い仮想サーバが使えるコンテナ型の仮想化ソフトウェアのDockerについてインストールしてみました。

構成

Ubuntu 12.04 : Docker をインストール
※上記のサーバはVMware ESXi 5.1上で動作しています。

インストール

公式サイトにある手順を参考にDockerをインストールします。

まずは依存関係のあるパッケージをインストールし、サーバを再起動します。
他のサイトも参考にパッケージは追加しています。

$ sudo apt-get update
$ sudo apt-get dist-upgrade
$ sudo apt-get install ssh git build-essential libssl-dev
$ sudo apt-get install linux-image-generic-lts-raring linux-headers-generic-lts-raring
$ sudo reboot

Dockerをインストールします。

$ curl -s https://get.docker.io/ubuntu/ | sudo sh

※warningが出た場合はlxc-dockerをインストールしてくれと書いてありましたが、上記でlxc-dockerも合わせてインストールされるようです。

動作確認

まず、dockerでコンテナ(仮想サーバ)を起動してログインしてみます。
以下の場合、イメージ名がubuntuのコンテナを起動後、bashを実行し、ログインしています。

  • iオプションはインタラクティブモードでの起動を示しています。
  • tオプションは仮想端末ttyの割り当てを示しています。
$ sudo docker run -i -t ubuntu /bin/bash
$ root@11ab5172a0ec:/# uname -a
Linux 11ab5172a0ec 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 
$ root@11ab5172a0ec:/# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04 LTS"
$ exit

コンテナにログイン後、サーバのバージョンを確認してみましたが、Ubuntu14.04が起動していることが分かります。

次に、コンテナの一覧を確認します。
先ほど起動したコンテナが表示されていることが分かります。

$ sudo docker ps -a
CONTAINER ID        IMAGE                COMMAND                CREATED             STATUS                      PORTS                                          NAMES
11ab5172a0ec        ubuntu:14.04         /bin/bash              58 minutes ago      Exited (0) 56 minutes ago                                                  condescending_thompson

上記のコンテナからログオフし、再度コンテナを起動すると、別のコンテナとして表示されます。
コンテナの変更を保存するため、以下のコマンドを実行します。

$ sudo docker commit <コンテナID> <イメージ名> #コンテナIDはdocker psで表示された11ab5172a0ecが対応します。

Gitと似ていますが、上記のコマンドを実行することで、コンテナをイメージに変換することができます。

よく使うコマンド

dockerでよく使うコマンドについてまとめてみました。

$ sudo docker ps -a #コンテナの一覧を表示
$ sudo docker rm <コンテナID> #コンテナの削除
$ sudo docker images #イメージの一覧を表示
$ sudo docker rmi <イメージID> #イメージを削除

SSH接続

コンテナにSSH接続するには、コンテナにsshdをインストールします。
今回は、イメージにUbuntu12.04を指定しました。

$ sudo docker run -i -t ubuntu:12.04 /bin/bash
root@b9da36681214:/# apt-get update
root@b9da36681214:/# apt-get install openssh-server
root@b9da36681214:/# mkdir /var/run/sshd
root@b9da36681214:/# /usr/sbin/sshd
root@b9da36681214:/# passwd root
root@b9da36681214:/# exit

上記で作成したコンテナをcommitし、イメージにします。

$ sudo docker ps -a #コンテナIDを確認します。
$ sudo docker commit <コンテナID> ubuntu-sshd

次に、以下のようにubuntu-sshd コンテナを使って /usr/sbin/sshdで実行します。

  • pオプションはポートフォワードに使うポート番号を指定しています。

SSHでは22番ポートを使用しているため、22番ポートをフォワードします。
このオプションは複数指定することが可能で、22番ポートと80番ポートを使いたい場合は、-p 20 -p 80と指定します。

$ sudo docker run -d -p 22 ubuntu-sshd /usr/sbin/sshd -D

以下のコマンドで、PORTSの列に「0.0.0.0:[ポート番号]」が表示されますが、これが22番ポートにマッピングされたローカルのポートになります。

$ 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

上記で確認したポート番号を指定して、コンテナにSSH接続します。

$ ssh root@127.0.0.1 -p [ポート番号]

今回の構成は、VMware ESXi 5.1上のUbuntu12.04のサーバに、Ubuntu12.04のコンテナを起動しましたが、評判通りコンテナの起動は早く、サーバの負荷は小さいものでした。Chefやserverspecの環境として、使用できる見通しが取れたかと思います。

Chef + serverspec + Jenkins でサーバ構築からテストまでを自動化してみた

Chefとserverspecでサーバ構築とテストをプログラマブルにできるようになったため、Jenkinsによりサーバ構築からテストまでを自動化してみました。

やりたいこと

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

  1. Chefのレシピをリモートリポジトリへgit pushすると、Jenkinsが通知を検知
  2. JenkinsからChefを実行し、サーバを構築
  3. サーバの構築が正常終了すれば、Jenkinsからserverspecを実行し、サーバの状態をテスト

構成

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

インストール

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

設定

各ソフトウェアの設定について説明していきます。
SSH認証の設定は完了しているものとします。

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 :apache2
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

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]をクリックします。

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

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

  • ソースコード管理
    • 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のコマンドを実行するために、[ビルド手順を追加]>[シェルの実行]をクリックし、[シェルの実行]を以下のように設定します。

sudo /root/.rbenv/shims/knife solo bootstrap <ユーザ名>@192.168.xxx.xxx
sudo /root/.rbenv/shims/rake spec

※bootstrapはprepareとcookを同時に実行するオプションです。

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

実行

以下のように、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」と表示されます。

以下は、Chefとserverspecの実行が正常終了していることを示しています。

SCMのポーリングが実行
ビルドします。 ワークスペース: /var/lib/jenkins/workspace/chef-cookbooks
Fetching changes from the remote Git repository
Fetching upstream changes from /var/lib/git/chef-cookbooks.git
Checking out Revision 187f8b995de5ddc053a18a3d114f1aa5018f1979 (origin/master)
[chef-cookbooks] $ /bin/sh -xe /tmp/hudson7478251361203469042.sh
+ sudo /root/.rbenv/shims/knife solo bootstrap <ユーザ名>@192.168.xxx.xxx
Running Chef on 192.168.xxx.xxx...
Checking Chef version...
:
Running handlers complete
[0m
Chef Client finished, 1/1 resources updated in 8.041423413 seconds[0m
+ sudo /root/.rbenv/shims/rake spec
/root/.rbenv/versions/2.0.0-p451/bin/ruby -S rspec spec/192.168.xxx.xxx/httpd_spec.rb
.

Finished in 0.10092 seconds
1 example, 0 failures
Finished: SUCCESS

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

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

Chefの実行で失敗した場合は、serverspecは実行されないため、正常終了すればサーバ構築からテストまですべて終了したことになります。

おわりに

今回構築した環境は、共同でChefのレシピやserverspecのテストコードを書くフローを想定していました。各メンバーは、PCでChefやserverspecのコードを作成後、VirtualBoxでテストし、問題がなければリモートリポジトリへgit pushします。リモートリポジトリへgit pushすれば、後はJenkinsがサーバ構築からテストまでを自動で行い、正常終了すれば本番環境へ適用するといった流れです。ただし、今回の環境ではテスト環境をクリアすることは含まれていないため、スナップショットを手動で戻してやる手間が残ってしまいました。
次回は、このあたりの問題を解決していきたいと考えてます。

serverspecで構築したサーバの状態をテストする

Chefでプログラマブルなサーバ構築ができるようになったため、構築後のサーバの状態をプログラマブルにテストするために、serverspecをインストールしてみました。

構成

  • CentOS 6.5 : serverspecをインストール
  • Ubuntu 12.04 : テスト対象

インストール

公式サイトにある手順を参考にserverspecをインストールします。
Rubyはすでにインストールされているものとします。

gemからserverspecをインストールします。

$ gem install serverspec

初期設定

セッティング

serverspecのセッティングを行います。
任意のディレクトリに移動し、以下のserverspec-initコマンドを実行します。
※Chefのrootディレクトリ(chef-repo)でもよいかもしれません。
対話形式で聞いてくるので、以下のように設定します。

$ mkdir serverspec
$ cd serverspec
$ 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

SSH認証

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

$ ssh-keygen
$ scp ~/.ssh/id_rsa.pub <ユーザ名>@<テスト対象サーバのIPアドレス>:
$ ssh <ユーザ名>@<テスト対象サーバのIPアドレス> # テスト対象サーバへログイン
$ mkdir ~/.ssh
$ mv id_rsa.pub ~/.ssh/authorized_keys

パスワードなしでsudoを実行する設定を行います。

$ sudo visudo
<ユーザ名> ALL=(ALL) NOPASSWD: ALL # ←最終行に追加
$ exit

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

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

テスト

テストコード

serverspec-init後に、テストコードのサンプルとしてspec/192.168.xxx.xxx/httpd_spec.rbが作成されます。httpd_spec.rbを参考に、以下のようにテストコードを書きます。

$ 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

※require 'spec_helper'でspec/spec_helper.rbが実行され、~/.ssh/configからSSH接続の情報を取得しています。

テスト実行

以下のコマンドを実行し、テストを実行します。
exampleはテストの件数、failuresは失敗したテストの件数を示しています。
以下の場合は、全てのテストが成功したことを示しています。

$ rake spec
/root/.rbenv/versions/2.0.0-p451/bin/ruby -S rspec spec/192.168.xxx.xxx/apache2_spec.rb
.

Finished in 0.10092 seconds
1 example, 0 failures

後は、spec/192.168.xxx.xxx/配下にテスト対象ごとに*_spec.rbファイルを作成し、テストコードを書いていきます。

Gitのインストールと初期設定

バージョン管理を行うため、CentOS6.5にGitをインストールしてみました。

インストール

公式サイトにある手順を参考にGitをインストールします。
最新版のGitを使用する場合は、ソースからのインストールしますが、今回はyumからインストールしました。

$ yum -y install git

初期設定

ユーザの登録

まずは、Gitのユーザ名とメールアドレスを登録します。
「--global」オプションを付けることで、「~/.gitconfig」に情報が格納されます。

$ git config --global user.name <ユーザ名>
$ git config --global user.email <メールアドレス>
$ git config --list

リモートリポジトリの作成

リモートリポジトリを作成します。
リモートリポジトリへのアクセスは、一旦Localプロトコルで行います。
他サーバから接続する場合は、SSHプロトコルやGitプロトコルが使えます。SSHプロトコルを使う場合は、SSH認証の設定が必要となります。

$ mkdir /var/lib/git
$ mkdir /var/lib/git/project.git
$ cd /var/lib/git/project.git
$ git --bare init

ローカルリポジトリの作成

管理対象のディレクトリに移動し、リポジトリの初期化を行い、ローカルリポジトリを作成します。

$ mkdir local
$ cd local
$ git init
$ echo test > test #テスト用のファイルです
$ git add .
$ git commit -m "first commit"

リモートリポジトリには何も入っていないため、ローカルリポジトリの内容を追加します。※Localプロトコルでアクセスしています。

$ git remote add origin /var/lib/git/project.git
$ git push origin master

初期設定は以上となります。

Gitの簡単な使い方として、ファイルの編集→add→commit→pushを繰り返し行っていくことになります。
Gitの使い方については、まだ分からない点があり、整理ができたところで記事にしたいと思っています。

GitWebのインストール

インストール

Gitはコマンドベースのツールのため、リポジトリがどのような状態になっているか分かりにくいときがあります。
そこで、Gitのリモートリポジトリをブラウザで見れるように、GitWebをインストールします。
apacheも必要であるため、合わせてインストールします。

$ yum -y install httpd
$ yum -y install gitweb

/etc/gitweb.confファイルにリモートリポジトリのパスを指定します。

$ vi /etc/gitweb.conf
#our $projectroot = "/var/lib/git";
our $projectroot = "/var/lib/git"; ←コメントアウトを外して、リモートリポジトリのパスを指定

apache自動起動の設定とサービスを起動します。

$ chkconfig httpd on
$ service httpd start

apacheのデフォルトポートは80なので、ファイアウォールの設定によりポート80が使えるように開放します。

$ vi /etc/sysconfig/iptables
-A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT ←追加します
$ service iptables restart

また、SELinuxが動作していると、「404 - No projects found 」と表示されGitの情報が見れない場合があります。以下のファイルを修正し、SELinuxを無効化します。

$ vi/etc/sysconfig/selinux
#SELINUX=enforcing
SELINUX=disabled

サーバを再起動すると設定が反映されます。

デザインの変更

GitWebのデザインをGitHub風に変更するテーマが公開されてるので適用します。
(見た目は大事です)

$ cd /usr/local/src/
$ git clone https://github.com/kogakure/gitweb-theme.git
$ cd gitweb-theme/
$ \cp -rf ./* /var/www/git

※\cpはcp -iのエイリアスを無効にして、上書き確認を無効にしています。

Jenkinsのインストールと初期設定

継続的インテグレーション(CI:Continuous Integration)を行うため、CentOS6.5にJenkinsをインストールしてみました。

インストール

JenkinsはJavaで動作しているため、Javaをインストールします。

$ yum -y install java-1.7.0-openjdk

公式サイトにある手順を参考にJenkinsをインストールします。

$ wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
$ rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
$ yum -y install jenkins

Jenkinsの自動起動の設定とサービスを起動します。

$ chkconfig jenkins on
$ service jenkins start

Jenkinsのデフォルトポートは8080なので、ファイアウォールの設定によりポート8080が使えるように開放します。

$ vi /etc/sysconfig/iptables
-A INPUT -m state --state NEW -m tcp -p tcp --dport 8080 -j ACCEPT ←追加します
$ service iptables restart

端末からブラウザで「http://localhost:8080」にアクセスできることを確認します。

初期設定

初回アクセス後、[Jenkinsの管理]をクリックすると、以下のメッセージが出力されます。
初期の状態では誰でもジョブの設定ができるため、セキュリティの設定を行います。

セキュリティを有効化しないと、ネットワーク上の誰にでもプロセスの起動を許可します。悪用を防ぐために、少なくとも認証を有効化することを検討してください。

[Jenkinsの管理]>[グローバルセキュリティの設定]へ移動します。
セキュリティを有効化のチェックボックスをオンにすると、設定画面が表示されます。
セキュリティを有効化のオプションを以下のように設定し、保存をクリックします。

  • Jenkinsのユーザーデータベース チェック
    • ユーザーにサインアップを許可 チェック
  • ログイン済みユーザーに許可  チェック

ログイン画面に遷移するので、[アカウント登録]をクリックし、[サインアップ]に行きます。
以下のアカウント情報を入力し、[サインアップ]クリックします。

  • ユーザ名
  • パスワード
  • パスワードの確認
  • フルネーム
  • メールアドレス

「ログインしました。トップに戻ります。」と表示されるので、[トップ]をクリックします。
トップ画面に遷移すると、上記のアカウントでログインしている状態となります。
「Jenkinsの管理」をクリックすると、上記の「セキュリティを有効化しないと~」のメッセージが消えていることが分かります。

[Jenkinsの管理]>[グローバルセキュリティの設定]へ移動します。
セキュリティを有効化のオプションを以下のように変更し、保存をクリックします。

  • Jenkinsのユーザーデータベース チェック
    • ユーザーにサインアップを許可 チェックオフ
  • 行列による権限設定 チェック
    • 作成したユーザ名を入力して追加をクリック
      • ユーザ名の行のチェックを全てオン

一旦ログオフし、ログインできるか確認します。
※[セキュリティを有効化]の設定で失敗すると、ログインできないため注意が必要です。(失敗した場合は、再インストールしたほうが早いかもしれません。)

プラグインのアップデート

[Jenkinsの管理]>[プラグインの管理]へ移動すると、[アップデート]タブに初期導入されていたプラグインのうちアップデートが可能なプラグインが表示されます。
すべてのプラグインにチェックを入れ、[ダウンロードして再起動後にインストール]をクリックします。
アップデート後、[Jenkinsの管理]>[プラグインの管理]へ移動すると、[アップデート]タブにプラグインが表示されていないことを確認します。

Gitの設定

初期ではGitのプラグインがインストールされていないため、JenkinsからGitを使いたい場合は、Gitのプラグインをインストールします。

[利用可能]タブへ移動し、「Git Plugin」にチェックを入れ、[ダウンロードして再起動後にインストール]をクリックします。

  • Git Plugin チェック

※チェックをしていなくても「Git Client Plugin」も合わせてインストールされます。

次に、[Jenkinsの管理]>[システムの設定]へ移動すると、Gitの項目が表示されています。
「Path to Git executable」にGitのパスを入力し、[保存]をクリックします。

  • Name: Default
  • Path to Git executable: /usr/bin/git


後は、[新規ジョブ作成]をクリックして、ジョブを作成していきます。