CircleCI で docker + serverspecを実行する
前回はローカルで docker 上で serverspecを実行した。
今回は、CircleCI上で dockerを起動し、knife-soloで構築し、
serverspecでテストができるようにしようと思う。
全体のワークフローとしては、以下の順序となる。
- Githubにchefのコードをpushする
- CircleCI上でdockerを起動する
- dockerのimageを作成し、コンテナを起動。
- そのコンテナに対し、knife-soloを実行し、インフラを構築。
- 構築したコンテナに対して、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での流れとしては、
- dockerのサービスを起動
- dockerでログインするホストに対しcircleというHost名を設定する。
(knife-soloでホスト名.jsonで使用するのと、serverspecで、テスト先のホスト名を使用するため) - knife-soloでsshでログインするので、ログインするための秘密鍵と公開鍵を作成。
Dockerfileでauthorized_keysを設定するため、公開鍵をauthorized_keysとしてコピー。
Dockerfileを使用して、docker imageを作成し、コンテナ実行。
- 作成したコンテナにknife-soloを実行。
- 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で作成したコンテナに対して、実行するようにしたい。
ちなみに環境は、
- Mac OSX 10.9
- VirtualBox(Vagrant)にboot2dockerをインストール
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のコードがプッシュされたときに自動でサーバーの構築され、テストが走るはず。
ためしてないけど。
おしまい。
参考サイト
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.rb
にAPIキーと通知する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を使って、通知の設定と監視項目を追加
を行っていく予定。
scalaで新規プロジェクトを作成する
griterの使い方をいつも忘れてしまうので、メモ。
$ g8 rkmathi/basic-project
とすると、
- プロジェクト名
- 組織
- version
を聞かれるので入力すれば、プロジェクトが作成される。