Chef(knife-solo)+Serverspec+GitHub+Wercker+AWS(EC2)でインフラCIする
ChefとServerspecやったらこれはインフラCI試してみるしかないということでやってみた。
CIするためのツールとかサービスとかいろいろあって、CIツールとしてはJenkinsが有名なんだけど、例えばJenkins用にEC2のインスタンス立て維持するのは個人としてはコスト的にちょっとなーというかんじではある。個人ならCIサービスのがよいかなーと調べてみたところ、だいたい機能にはあんまり大差なさそうな感じでTravisとか有名そうだったんだけど、Werckerというのがプライベートリポジトリも無料っぽくて、後々何かの役に立ちそうな気もするのでWerckerにした。ただベータ期間中だけ無料ということであって、将来的にもプライベートリポジトリが無料かどうかはわかんないです。
やってみたツールの組み合わせはChef(knife-solo)+Serverspec+GitHub+Wercker+AWS(EC2)というかんじで、ChefとServerspecで書いたインフラコードをGitHubにプッシュするとWerckerでフックして、EC2上にインスタンス立てて、Chefのレシピを適用し、Serverspecでテストするという流れ。
大まかな流れについては、サーバ/インフラ徹底攻略 (WEB+DB PRESS plus) 「特集2 テスト駆動インフラ&CI最前線」で、Puppet+Serverspec+GitHub+Wercker+Vagrant+DigitalOceanの例が載ってて、基本的な構想はこれを参考にしたのだけど、サーバのキッティングがPuppetじゃなくてChefなのと、テストのインスタンス立てるのがDigitalOceanじゃなくてAWS。あとクラウド上にインスタンス立てるのに作りがVagarantに依存するのが個人的にイマイチな気がして、AWSの制御はaws-sdkからAPIを自前で叩くかんじにしたので、それぞれの部品はいろいろ違うかんじにはなった。
まず事前準備として、AWSコンソールからwercker用のIAMユーザを作って、EC2のインスタンス制御するためのAPI用のアクセスキーを生成する。
- IAM
- Groups
- Create New Group
- Group Name: ec2-admins
- Attach Policy: AmazonEC2FullAccess
- Create New Group
- Users
- Groups
あと、EC2インスタンスにSSHログインするためのSSH鍵を作っておく。
- EC2
- Key Pairs
- Create Key Pair
- Key pair name: aws-wercker
- Create Key Pair
- Key Pairs
秘密鍵をaws-wercker.pemとしてダウンロードしておく。SSH鍵についてはWercker側でキーペア作成することもできるのだけど、その場合は、秘密鍵がWerckerのサービスの外に持ち出せないので、ビルドがコケた時に手元のPCからビルド中のインスタンスにSSHできなくてイマイチなので、AWS側で作ったキーをWerckerに登録した方が柔軟性があると思う。
次に、Werckerにアカウント登録する。
http://wercker.com/
登録時にREGISTER USING GITHUBでGitHubアカウント認証もしておく。
ログインしたらダッシュボードから「Add An Application」を選んで、リポジトリを登録する。
- Add An Application
リポジトリ登録したらSettingsタブで環境変数とか設定する。
- Settings
ビルド定義のwercker.ymlはこんなかんじ。GitHubのリポジトリの直下に配置しておく。
box: wercker/ruby build: steps: - bundle-install - script: name: make ssh dir code: mkdir -m 700 -p $HOME/.ssh - create-file: name: put ssh private key filename: $HOME/.ssh/aws-wercker.pem overwrite: true hide-from-log: true content: $AWS_SSH_KEY_PRIVATE - script: name: chmod ssh private key code: chmod 400 $HOME/.ssh/aws-wercker.pem - script: name: delete ec2.env file if exists code: rm -f ec2.env - script: name: aws ec2 run-instances code: bundle exec ruby ec2-run-instances.rb - script: name: print ec2.env for debug code: cat ec2.env - script: name: export environments code: source ec2.env - script: name: ssh connect test code: ssh ec2-user@$TARGET_IP -i $HOME/.ssh/aws-wercker.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null hostname - script: name: berks vendor cookbooks code: bundle exec berks vendor cookbooks - script: name: knife solo bootstrap code: bundle exec knife solo bootstrap ec2-user@$TARGET_IP -i $HOME/.ssh/aws-wercker.pem -N commitm-ap - script: name: serverspec code: bundle exec rake serverspec:commitm-ap - script: name: aws ec2 terminate-instances code: bundle exec ruby ec2-terminate-instances.rb
順番に簡単に説明していくと、
box: wercker/ruby
boxというのはwercker上でビルドする環境のことで、予めいくつか用意されてるし、自分で作って登録することもできるようです。
build: steps: - bundle-install
次にビルドのステップを順番に書いていきます。
bundle-installは組み込みタスクでGemfileをbundle installするやつです。
ちなみに今回Gemfileはこんなかんじです。
source 'https://rubygems.org' gem "chef", '~> 12.0.3' gem "knife-solo", '~> 0.4.2' gem "berkshelf", '~> 3.2.3' gem "serverspec", '~> 2.8.2' gem "aws-sdk", '~> 2.0.28'
各自、必要な物を適宜書いてください。
- script: name: make ssh dir code: mkdir -m 700 -p $HOME/.ssh - create-file: name: put ssh private key filename: $HOME/.ssh/aws-wercker.pem overwrite: true hide-from-log: true content: $AWS_SSH_KEY_PRIVATE - script: name: chmod ssh private key code: chmod 400 $HOME/.ssh/aws-wercker.pem
SSHの秘密鍵を配置するのは環境変数を参照してファイルに落とす。scriptタスクのcodeで任意のコマンドが書けるので基本なんでもできる。nameの欄はただのコメントなのでなんでもいいけど、werckerの管理画面でビルド中の進捗状況にnameの文字列が出るので処理内容がわかる程度のコメントの方がよいと思う。
- script: name: delete ec2.env file if exists code: rm -f ec2.env - script: name: aws ec2 run-instances code: bundle exec ruby ec2-run-instances.rb - script: name: print ec2.env for debug code: cat ec2.env - script: name: export environments code: source ec2.env
aws-sdkでEC2インスタンス作ってるec2-run-instances.rbの中身は昨日のエントリを参照。
EC2インスタンス起動後にsshできるまで待つ(若干手抜き) - 城陽人の本棚
立てたインスタンスのIDとパブリックIPを他のステップに環境変数で引き回すために、ec2.envという中間ファイル作ってます。なんかもうちょっとカッコいい方法ないものか。
- script: name: ssh connect test code: ssh ec2-user@$TARGET_IP -i $HOME/.ssh/aws-wercker.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null hostname
後続のchefの適用などがコケた場合の切り分けのために、念のためSSH接続のテストしておく。
- script: name: berks vendor cookbooks code: bundle exec berks vendor cookbooks - script: name: knife solo bootstrap code: bundle exec knife solo bootstrap ec2-user@$TARGET_IP -i $HOME/.ssh/aws-wercker.pem -N commitm-ap
berksでクックブック取得してknife soloで適用する。-Nがレシピを適用するノード定義。ファイル名がnodes/commitm-ap.jsonとかであらかじめ定義してあります。
{ "environment": "production", "run_list": [ "role[base]", "role[ap]" ] }
ロールの定義の詳細は本題からそれるので割愛。Chefの使い方は適宜ググってください。
- script: name: serverspec code: bundle exec rake serverspec:commitm-ap
serverspecでテストする。serverspecのテスト対象ごとに変わるパラメータの指定はhosts.jsonみたいな設定を外出ししたファイルを予め準備しておいて、rakeタスク生成したタイミングで環境変数埋め込むようなかんじにしてます。
[ (略) { "name": "commitm-ap", "host_name": "<%= ENV['TARGET_IP'] %>", "user": "ec2-user", "port": 22, "keys": "<%= ENV['TARGET_SSH_KEYPATH'] %>", "roles":["base", "ap"] } ]
Rakefileの書き方とかは以下を参照。
Serverspecでテスト対象のIPとロールを指定する - 城陽人の本棚
knife-soloとの対称性を考えると環境変数よりrakeタスクの引数にした方が綺麗かもしんない。
- script: name: aws ec2 terminate-instances code: bundle exec ruby ec2-terminate-instances.rb
最後にEC2のインスタンスを廃棄する。インスタンスの廃棄は普通のstepsの中に書かずにafter-stepsというのでfinallyっぽい書き方もできるのだけど、ビルドこけたら現状保存した状態でsshログインして調査したくなるので、普通のstepsに直列に並べてます。ec2-terminate-instances.rbの中身はこんなかんじで、aws-sdkからterminate_instances叩いてるだけです。消す前にAMIとか作成したかったらAWSのAPI好きに叩けばよいと思う。
require 'aws-sdk' # set environments puts "initialize client" ec2 = Aws::EC2::Client.new( access_key_id: ENV['AWS_ACCESS_KEY_ID'], secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], region: ENV['AWS_DEFAULT_REGION'] ) instance_id = ENV["INSTANCE_ID"] puts "INSTANCE_ID=#{instance_id}" puts "terminate instance" terminate_response = ec2.terminate_instances(instance_ids: [instance_id]) puts "#{terminate_response}"
ビルド定義をwercker.ymlという名前でリポジトリの直下に置いておいたら、あとは何かリポジトリを更新してgit pushするたびに自動でビルドが走る。うん、いいかんじ。
werckerならではっぽい注意点としては、ビルドは25分以内に終わる必要がある。ただbundle-installはキャッシュされるので、初回だけ時間かかっても2回目以降はキャッシュが聞くのでだいぶ速い。あと、ビルド中標準出力に5分以上出力がないとハングしたと見なされて強制停止される。これが原因でberkshelfが依存してるdep-selector-libgecodeのインストールが一回コケたのだけど、再ランしたらキャッシュが効いたからか通った。本質的に解決してないのであんまりよくはないと思うので、もし頻発して前処理にいろいろ時間がかかる場合は専用のBOXを作った方がよいかもしんない。あとEC2インスタンスの起動待ちとか時間のかかりそうなタスクは適宜ループ処理内で標準出力を出したりして若干の工夫が必要。
まあ何にせよこれでとりあえずミニマムなインフラCI環境できたのでよいかんじじゃないでしょうか。
参考
- サーバ/インフラ徹底攻略 (WEB+DB PRESS plus) 特集2 テスト駆動インフラ&CI最前線
- http://masutaka.net/chalow/2014-09-14-1.html