CircleCI で docker + serverspecを実行する

前回はローカルで docker 上で serverspecを実行した。
今回は、CircleCI上で dockerを起動し、knife-soloで構築し、 serverspecでテストができるようにしようと思う。

全体のワークフローとしては、以下の順序となる。

  1. Githubにchefのコードをpushする
  2. CircleCI上でdockerを起動する
  3. dockerのimageを作成し、コンテナを起動。
  4. そのコンテナに対し、knife-soloを実行し、インフラを構築。
  5. 構築したコンテナに対して、serverspecを実行し、テストを行う

そうすると、Githubの Merge Pull Request ボタンにテストの結果が表示される。

CircleCIの始め方などは、割愛。

Dockerfileを使用して、dockerイメージを作成する

まず、以下のDockerfileを作成し、ssh可能なコンテナを作成できるようにする。

FROM centos:centos6

RUN yum install -y passwd
RUN yum install -y openssh
RUN yum install -y openssh-server
RUN yum install -y openssh-clients
RUN yum install -y sudo

RUN useradd moock
RUN passwd -f -u moock
RUN mkdir -p /home/moock/.ssh; chown moock /home/moock/.ssh; chmod 700 /home/moock/.ssh
ADD authorized_keys /home/moock/.ssh/
RUN chown moock /home/moock/.ssh/authorized_keys; chmod 600 /home/moock/.ssh/authorized_keys

RUN echo "moock    ALL=(ALL)       ALL" >> /etc/sudoers.d/moock

RUN sed -i -e "s:^UsePAM yes$:#UsePAM yes:" /etc/ssh/sshd_config
RUN /etc/init.d/sshd start
RUN /etc/init.d/sshd stop

注意点としては、sshd_configのファイルでPAMを使用する設定になっていたが、 こうすると、ログインした後すぐにログアウトしてしまったので、PAMを使用しないようにした。

また、authorized_keysはcircle.ymlで作成する。

CircleCIの設定

次にCircleCIの設定を行う。

circle.yml は以下のように設定した。

machine:
  ruby:
    version: 2.0.0
  timezone: Asia/Tokyo
  services:
    - docker

dependencies:
  post:
    - echo "Host circle" >> ~/.ssh/config
    - echo "  HostName 127.0.0.1" >> ~/.ssh/config
    - echo "  User moock" >> ~/.ssh/config
    - ssh-keygen -N "" -t rsa -f ~/.ssh/id_rsa
    - cp ~/.ssh/id_rsa.pub authorized_keys

test:
  pre:
    - docker build -t moock/sshd .
  override:
    - cd chef-repo && container_id=`docker run -d -p 22 moock/sshd /usr/sbin/sshd -D` && ssh_port=`docker inspect --format='{{ if index .NetworkSettings.Ports "22/tcp" }}{{ (index (index .NetworkSettings.Ports "22/tcp") 0).HostPort }}{{  end }}' $container_id` && bundle exec knife solo prepare circle -p $ssh_port && bundle exec knife solo cook circle -p $ssh_port && bundle exec rake serverspec:circle SSH_PORT=$ssh_port:
      timeout: 1200

testの項目をoverrideし、knife-soloの実行とserverspecの実行を行っている。

CircleCIでの流れとしては、

  1. dockerのサービスを起動
  2. dockerでログインするホストに対しcircleというHost名を設定する。
    (knife-soloでホスト名.jsonで使用するのと、serverspecで、テスト先のホスト名を使用するため)
  3. knife-soloでsshでログインするので、ログインするための秘密鍵と公開鍵を作成。
  4. Dockerfileでauthorized_keysを設定するため、公開鍵をauthorized_keysとしてコピー。

  5. Dockerfileを使用して、docker imageを作成し、コンテナ実行。

  6. 作成したコンテナにknife-soloを実行。
  7. serverspecでテスト。

という感じになる。

knife-solo周りのコマンドは、前回のを流用。

ハマった点としては、test項目で timeout の設定をしないと、標準出力に3分間出力がないと timeoutエラーになってしまうところ。
chefでrbenvなどを使って、rubyコンパイルしたりすると、確実にtimeoutエラーになってしまうので、 タイムアウトの時間を20分に設定した。

あと、なんか汚いからもう少しきれいにしたい。。

knife-soloの設定

CircleCIで実行するレシピを指定する。ファイル名は .ssh/config で指定したホスト名を使用するため、 ここでは、circle.jsonとなる。

例として、nginxのレシピを作成し、実行する。

{
  "run_list":[
    "recipe[nginx]"
  ]
}

serverspecの設定

serverspecの設定としては、例としてspec/web/nginx_spec.rb を作成し、 property.ymlにテストする項目として追加する。

circle:
  :roles:
    - web

これで、Githubにchefのコードをpushすれば、自動でテストを行うようになった。

改善点

一応、環境は構築できたのだが、改善点がある。

Dockerfileを使用してコンテナを作成しているが、ここの部分はそれなりに時間がかかるし、毎回作成するのは、やっぱり無駄っぽい。
たぶんこの辺は、DockerHubを使用すれば、解決するのではないかと思う。

Docker + Serverspecでインフラのテストを行う

knife-soloを使ってサーバー構築しているのだが、serverspecを使ってテストをしていなかった。
でも、前々からしようと思っていたので、今回重い腰を上げて実際にserverspecを使ってテストを行うようにした。

基本的な方針として、ローカル環境では knife-solo + docker を使用しているので、それにあわせて serverspecもknife-soloで作成したコンテナに対して、実行するようにしたい。

ちなみに環境は、

Serverspecをインストール

chefのプロジェクトのGemfile に serverspecを追加し、公式サイトに従いインストール。 まあ、この辺は色々と情報があるので、割愛。

また、特に必要と言うわけではないが、~/.ssh/configに dockerを使用するホストのIPアドレス とユーザー名を指定。IPアドレスは環境によって違うので、自分の環境に合わせる。

Host docker
  HostName        192.168.42.43
  User            moock

Serverspecをroleに分ける

ローカル環境、ステージング環境、本番環境とあるのでホスト名でディレクトリを分けるのではなく、 ロールで分けるようにした。

この辺は、公式サイトに記述されているように、 以下のようなイメージ。

 spec
  ├── base
  │   └── base_spec.rb
  ├── db
  │   └── mysql_spec.rb
  ├── solr
  │   └── solr_spec.rb
  └── web
      └── nginx_spec.rb

また、ホストとロールを関連づけるため、以下のようなYAMLファイル (properties.yml) を作成する。

docker:
  :roles:
    - base
    - db
    - solr
    - web

そして、作成したテスト実行時に読み込むため、Rakefileに以下を記述する。

require 'rake'
require 'rspec/core/rake_task'
require 'yaml'

properties = YAML.load_file('spec/properties.yml')

desc "Run serverspec to all hosts"
task :spec => 'serverspec:all'

namespace :serverspec do
  task :all => properties.keys.map {|key| 'serverspec:' + key.split('.')[0] }
  properties.keys.each do |key|
    desc "Run serverspec to #{key}"
    RSpec::Core::RakeTask.new(key.split('.')[0].to_sym) do |t|
      ENV['TARGET_HOST'] = key
      t.pattern = 'spec/{' + properties[key][:roles].join(',') + '}/*_spec.rb'
    end
  end
end

こうすることにより、ホスト毎のテストのタスクが作成される。

$ bundle exec rake -T
rake serverspec:docker               # Run serverspec to docker
rake spec                            # Run serverspec to all hosts

Serverspecを特定のホストに対して実行する

rakeタスクでserverspecを実行すれば完成なのだが、dockerはコンテナを実行するたびに ポートが変わるので、rakeタスクを実行する際にポートを指定できるように、 spec_helper.rbに、ポートの部分を環境変数で指定するようにする。

require 'serverspec'
require 'pathname'
require 'net/ssh'

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

RSpec.configure do |c|
    c.request_pty = true
    c.host  = ENV['TARGET_HOST']
    options = Net::SSH::Config.for(c.host)
    options[:port] = ENV['SSH_PORT'] if ENV['SSH_PORT']
    user    = options[:user] || Etc.getlogin
    c.ssh   = Net::SSH.start(c.host, user, options)
    c.os    = backend(Serverspec::Commands::Base).check_os
end

こうすることにより、

bundle exec rake serverspec:docker SSH_PORT=49153

sshのポートを指定し、serverspecを実行できるようになる。 これで、dockerとserverspecを実行し、インフラをテストできるようになった。

おまけ

以下のスクリプトを実行すれば、まっさらな状態からインストールが始まり、 テストまで行われ、その後コンテナが廃棄される。

# sshdを起動させ、コンテナを実行
container_id=`docker run -d -p 22 <docker image> /usr/sbin/sshd -D`

# dockerのexposeされているsshのポートを取得
ssh_port=`docker inspect --format='{{ if index .NetworkSettings.Ports "22/tcp" }}{{ (index (index .NetworkSettings.Ports "22/tcp") 0).HostPort }}{{  end }}' $container_id`

# サーバーのプロビジョン & テスト(dockerは .ssh/config で指定したホスト名)
bundle exec knife solo prepare docker -p $ssh_port &&
bundle exec knife solo cook docker -p $ssh_port &&
bundle exec rake serverspec:docker SSH_PORT=$ssh_port

# コンテナの削除
docker stop $container_id
docker rm `docker ps -a -q`

また、これをjenkinsなどに登録し、githubと連携させれば、 chefのコードがプッシュされたときに自動でサーバーの構築され、テストが走るはず。
ためしてないけど。

おしまい。

参考サイト

Serverspec Advanced Tips

Sensuに通知処理を追加する

通知としては、メールとhipchatを使おうと思う。

プラグインこちらで 公開されているのでダウンロードし、 files/default/handlers 以下に プラグインを配置する。

プラグインには、#!/usr/bin/env ruby と記述されているが、
sensuインストール時に一緒にインストールされるrubyで実行するため、 一行目の #!/usr/bin/env ruby#!/opt/sensu/embedded/bin/ruby に変更する。

次に、recipe/server.rbに

remote_directory "/etc/sensu/handlers/" do
  source "handlers"
  owner "sensu"
  group "sensu"
  mode "0755"
  files_mode "0755"
end

を記述し、以後通知のプラグインを追加する際は、このディレクトリー以下に入れるようにする。

メールによる通知

attributes/server.rbにパラメータを追加する

default['mailer']['from'] = 'sensu@hoge.com'
default['mailer']['to'] = 'hoge@hoge.com'
default['mailer']['smtp_enable_starttls_auto'] = 'false'
default['mailer']['smtp_address'] = 'smtp.hoge.com'
default['mailer']['smtp_port'] = '25'
default['mailer']['smtp_domain'] = 'hoge.com'
default['mailer']['admin_gui'] = "http://xxxxxxxx.hoge.com"

TLSを使用しない場合は、smtp_enable_starttls_autoは falseでOK。 admin_guiは管理画面のURLを記述する。(メールの本文に表示される)

次にメール設定を行う処理をrecipes/server.rb に以下を追記する。 gemも必要だったので、併せてインストールするようにした。

sensu_handler "mailer" do
  type "pipe"
  command "mailer.rb"
end

execute 'install gem mail' do
  command "/opt/sensu/embedded/bin/gem install mail -v 2.5.4"
end

mailer = node['mailer']
sensu_json_file "#{node.sensu.directory}/conf.d/mailer.json" do
  owner "sensu"
  group "sensu"
  mode "0640"
  content({
    mailer: {
      mail_from: mailer['from'],
      mail_to: mailer['to'],
      smtp_enable_starttls_auto: mailer['smtp_enable_starttls_auto'],
      smtp_address: mailer['smtp_address'],
      smtp_port: mailer['smtp_port'],
      smtp_domain: mailer['smtp_domain'],
      admin_gui: mailer['admin_gui']
    }
  })
end

hipchatによる通知

defaults/hipchat.rbAPIキーと通知するRoomを指定する。

default['monitor']['hipchat']['apikey'] = ENV['HIPCHAT_API_KEY']
default['monitor']['hipchat']['room'] = 'sensu room'

hipchatのAPIキーをgitで管理するのは微妙だと思ったので、 あらかじめ環境変数に設定し、そこから取得するようにした。

ただ、knifeを実行する際に、sudo のオプションをつけないと、 環境変数が引き継げないので、そこは注意する。
(ただこのやり方だと、オプションを付け忘れた場合にブランクで上書きされるので、 ちゃんとするなら、consulなどを使った方がよいのかも?)

次にメールと同様に、hipchatを行う処理をrecipes/server.rb に以下を追記する。

execute 'install gem hipchat' do
  command "/opt/sensu/embedded/bin/gem install hipchat"
end

sensu_handler "hipchat" do
  type "pipe"
  command "hipchat.rb"
  severities ["ok", "critical"]
end

sensu_snippet 'hipchat' do
  content(
      :apikey => node['monitor']['hipchat']['apikey'],
      :room   => node['monitor']['hipchat']['room']
  )
end

chefを使って、uchiwaのインストールする

前回の続きで、 sensu-adminをインストールした 手順を公開しようかと思ったが、uchiwaを見かけて、こちらの方が見た目が良かったので、uchiwaをchefを使用してインストールしてみた。

Berkshelfに追加

cookbookが公開されていたので、そちらを使用する事にした。
Berksfileに以下を追記。

cookbook 'nginx'
cookbook 'uchiwa', '~> 0.4.3'

nginxは必須ではないが、プロキシとして使用するためインストール。

cookbookを作成する

nginxを使用せず、uchiwa単体で動かす場合は、Berksfileに追加してknifeを実行すれば、インストール完了し、 使用できるようになる。
しかし、今回はnginxと連携させるため、連携用のcookbookを作成する。

bundle exec knife cookbook create uchiwa -o site-cookbooks

recipes/default.rb は削除する。
(berkshelfでインストールしたnginxのdefault.rbを使用するため)
nginxに依存するので、metadata.rbに以下を追記する

depends 'nginx'

次にnginxをインストールするため、以下の内容のrecipes/nginx.rb を作成する。

include_recipe 'nginx::default'

# このテンプレートにuchiwaとの連携の設定を記述する。
template "#{node.nginx.dir}/conf.d/default.conf" do
  owner "root"
  group "root"
  mode 0644
  notifies :restart, "service[nginx]"
end

設定ファイルのテンプレートとして、templates/default/default.conf を作成し、以下を記述する。 最低限の設定をとりあえず記述。(ホストとポートくらいは外だしにした方が良いと思うが、今回は割愛。)

公式のサイトには、proxy_http_version 1.1; が書かれていたが、 設定に書くとnginxが起動しないので、とりあえず削除した。

 server {
  listen                80;
  server_name           _;

  location / {
    proxy_pass http://localhost:3000;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
  }
}

次に、uchiwaのデフォルトのrecipeと今回作成したnginxのrecipeを実行するための、 recipeを recipes/app.rbに作成。

include_recipe "#{@cookbook_name}::default"
include_recipe "#{@cookbook_name}::nginx"

でこれを、runlistに recipe[uchiwa::app] を追記して実行するれば、uchiwaのインストールが完了するはず。

アクセスすると、認証がかかっているので、

  • ユーザー名: admin
  • パスワード: supersecret

を入力すればログインできるはず。

使用感などは、また今度かな。

Sensu を使ってサービスを監視する

chefを使ってSensuをインストールしたかったが、ネットの情報だと インストール時のみにchefを使って、後は手動で設定していたり、 公式のcookbookのプログラムを直接編集していたりしていたが、 個人的にはそういった事はせずに、Sensuを管理していきたいと思った。

そこで、インスト−ル後の監視項目追加などもchefを使って管理し、 直接、公式のcookbookを編集しない方針でSensuのサービスを構築する事を 目的とし、ある程度構築できたので、メモとして残す事にした。

Sensuについての説明はいろんなサイトで紹介されているので、今回は割愛。

Sensuをインストールするためのcookboookを作成

まず、Sensuをインストールするためのcookbookを作成する。

$ mkdir chef-sensu-sample
$ cd chef-sensu-sample
$ bundle init

作成したGemfileには、以下を追記する。

gem 'knife-solo'
gem 'berkshelf'

記述したら、bundle install --path vendor/budle を実行し、インストールする。

次に、プロジェクトを作成する。

bundle exec knife init .

作成したら、公式のSensu のcookbookを使用するため、Berkfile に以下を追記する。

cookbook 'sensu'

以下のコマンドを実行し、cookbookをダウンロードしたら、こちらのサイトを参考にし、 ssl.jsonをdata_bags設定する。

$ bundle exec berks vendor cookbooks

次に、下記のコマンドを実行し、site-cookbook 以下に 独自の設定を記述するためのsensuのcookbookを作成する。

$ bundle exec knife cookbook create sensu -o site-cookbooks

sensu サーバーの設定

site-cookbooks/sensu/recipes 以下に server.rbを作成し、以下のように記述する

include_recipe "#{@cookbook_name}::default"
include_recipe "#{@cookbook_name}::rabbitmq"
include_recipe "#{@cookbook_name}::redis"
include_recipe "#{@cookbook_name}::server_service"
include_recipe "#{@cookbook_name}::api_service"
#include_recipe "#{@cookbook_name}::dashboard_service" 今回はインストールしない

dashbord_serverのレシピを実行するとWebUIで状況が見られるようになるが、 より高機能な sensu-admin を後でインストールするため、現時点ではインストールしない。

ここで、runlist に以下の設定を書いて実行することにより、サーバーの設定が完了する。

{
  "run_list":[
    "recipe[sensu::server]"
  ]
}

で、knifeを使って実行すれば、Sensuサーバーの基本的な設定は完了。

Sensu クライアント

サーバーと同様に recipes 以下に client.rb を作成し、

 include_recipe "#{@cookbook_name}::default"

sensu_client node.name do
  address node.ipaddress
  subscriptions node['sensu']['client']['subscriptions'] + ['all']
end

include_recipe "#{@cookbook_name}::client_service"

と記述する。

subscriptionsには、監視クライアントの種類を記述する。

クライアントのrunrlist には

{
  "sensu": {
    "client": {
      "subscriptions": []
    }
  },
  "run_list":[
    "recipe[sensu::client]"
  ]
}

を記述する。


今回はここまで。

次回以降、 - sensu-adminのインストール - chefを使って、通知の設定と監視項目を追加

を行っていく予定。