この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
最近はAnsible + Packerの組み合わせでAMIを作ることが増えてきました。毎回Ansibleを書き換えるごとにpackerコマンドを実行するのは面倒なので、最近はJenkinsを利用してAMI作成を自動化するようにしています。今日はそのご紹介です。
Jenkins + Packer環境の構築
Jenkins + Packerの構築は既に@ryuzeeさんがブログで大変丁寧に解説されていますので、そちらの手順を実施するだけで十分でしょう。私も大いに参考にさせて頂きました。ありがとうございます。
Jenkinsの準備ができたら実行する準備をしましょう。まず、プロジェクトのディレクトリ構成は以下のようになっています。
drwxr-xr-x 8 mochizukimasao staff 272 3 19 14:44 .
drwxr-xr-x 42 mochizukimasao staff 1428 3 2 16:29 ..
drwxr-xr-x 14 mochizukimasao staff 476 3 20 09:57 .git
drwxr-xr-x 13 mochizukimasao staff 442 3 17 15:25 ansible # Ansibleのディレクトリ
drwxr-xr-x 6 mochizukimasao staff 204 3 20 09:55 cfn # CloudFormation関連のディレクトリ
drwxr-xr-x 14 mochizukimasao staff 476 3 3 12:18 jenkins #上述したJenkins環境を構築するためのディレクトリ
drwxr-xr-x 5 mochizukimasao staff 170 3 19 13:28 jenkins_tools # Jenkinsで利用するスクリプトのディレクトリ
drwxr-xr-x 5 mochizukimasao staff 170 3 19 13:23 spec # テストを配置するディレクトリ
drwxr-xr-x 6 mochizukimasao staff 204 3 20 11:32 packer # Packer関連のディレクトリ
次にAMI生成のベースとなるPackerのテンプレートを書きます。
base.json
{
"variables": {
"aws_access_key": "",
"aws_secret_key": "",
"ami_name" : "",
"ami_id" : "",
"iam_instance_profile" : ""
},
"builders": [
{
"type": "amazon-ebs",
"access_key": "{{user `aws_access_key`}}",
"secret_key": "{{user `aws_secret_key`}}",
"region": "ap-northeast-1",
"source_ami": "{{user `ami_id`}}",
"instance_type": "c3.large",
"launch_block_device_mappings" : [
{
"device_name" : "/dev/xvda",
"volume_size" : "10",
"volume_type" : "gp2",
"delete_on_termination" : true
}
],
"availability_zone" : "ap-northeast-1a",
"iam_instance_profile" : "{{user `iam_instance_profile`}}",
"ssh_username": "ec2-user",
"ssh_timeout": "5m",
"tags" : {
"PackerName" : "{{user `ami_name`}}",
"Persistent" : "false"
},
"ami_name": "{{user `ami_name`}}_{{isotime | clean_ami_name}}"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sudo yum install -y --enablerepo=epel ansible"
]
},
{
"type": "ansible-local",
"playbook_dir" : "../ansible",
"playbook_file" : "../ansible/base.yml"
}
]
}
重要なのは、
- アクセスキーをテンプレートファイルに書かない(=IAM Roleを利用する)
- provisionerでansibleのディレクトリとplaybookファイルを指定する
の2点です。Packer + Ansibleの組み合わせについては以下のエントリも参照下さい。
あとはPackerの実行ですが、単純にPackerコマンドを叩くだけでも良いのですが、Packerに渡すパラメータ(例: AMI ID)をごにょごにょしたいことがあったので、Rakeでタスク化しました。
Rakefile
require 'aws-sdk-v1'
desc "build AMI using packer"
task :build
task :default => :build
task :build do
if ENV['target'].nil?
# build all templates if 'target' is not specified
template_files = Dir.glob('templates/*.json')
if template_files.count == 0
$stderr.puts "no template files found in templates/"
raise
end
template_files.each do |f|
exec_packer f
end
else # if target is specified
template_file = "templates/#{ENV['target']}.json"
unless File.exists? template_file
$stderr.puts "no template files found"
raise
end
exec_packer template_file
end
end
def exec_packer filename
opts = {}
unless ENV['profile'].nil?
# get AWS Credentials from shared_credential_files
begin
profile = AWS::Core::CredentialProviders::
SharedCredentialFileProvider.new(profile_name: ENV['profile'])
# For Packer
opts[:aws_access_key] = profile.credentials[:access_key_id]
opts[:aws_secret_key] = profile.credentials[:secret_access_key]
# For rake tasks
AWS.config(region: 'ap-northeast-1', credential_provider: profile)
rescue
$stderr.puts "no credential found from profile option. \nAWS_DEFAULT_KEY envvars will be used.\n"
end
else
AWS.config(region: 'ap-northeast-1')
end
opts[:ami_name] = File.basename(filename, ".json")
image_id = ""
ec2 = AWS::EC2.new
# Get Newest Amazon Linux HVM Image
AWS.memoize do
image_id = ec2.images.with_owner('amazon')
.filter("name", "amzn-ami-hvm-*-ebs")
.sort{|a, b| b.name <=> a.name }
.first.image_id
end
raise RuntimeError.new('No AMI found.') if image_id.empty?
puts "use #{image_id}"
opts[:ami_id] = image_id
args = ""
opts.each do |key, val|
args += " -var '#{key}=#{val}'"
end
command = "packer build #{args} #{filename}"
# packerコマンド実行
puts("executes : #{command}")
system(command)
raise RuntimeError.new if $?.exitstatus != 0
end
長いですが、やっていることは
- 最新のAmazon Linux AMIのAMI IDを動的に取得
- 指定があれば、AWSのアクセスキーを~/.aws/credentialsから取得
- 複数のAMIを作成したい場合は、templates/以下にjsonファイルを複数配置することで同時に実行
といったところです。これをpacker/のディレクトリ直下に配置します。
あとはGemfileも置いておいてあげましょう。
Gemfile
source "https://rubygems.org"
gem 'aws-sdk-v1'
gem 'rake'
ここまでやれば、あとはpacker/ディレクトリでbundle exec rake buildを実行するだけでPackerが動きます。
Jenkinsへの登録
ここまでできればあとはJenkinsに新規ジョブを登録してあげるだけです。今回は社内で利用しているGitサーバであるAtlassian Stashと連携させるため、Git連携設定を実施してあります。実行するスクリプトは以下の通りです。
#!/bin/bash
export PATH="${JENKINS_HOME}/bin:/usr/local/bin:$PATH"
sudo yum groupinstall -y "Development Tools"
sudo yum install -y ruby-devel
gem install bundler
cd packer
bundle install
bundle exec rake build
bundle installで追加されたものが${JENKINS_HOME}/binに、Packerが/usr/local/binに配置されるため、それらをPATHに追加しておきます。
その後必要なパッケージ群をインストールしておき、bundle installで依存Gemパッケージをインストールします。そして最後に先ほど紹介したbundle exec rake buildでAMIの作成ができます。Gitとの連携がうまくいっていれば、git pushするだけでAMIが自動で作られるのです。簡単ですね!
TIPS
上述した@ryuzeeさんのブログのなかでも触れられていましたが、Packerを実行しているとAMIが大量に作られて管理ができない状態になることがあります。そう言った状況を防ぐために定時でAMIを世代管理するRubyスクリプトを作っておきました。
purge_old_ami.rb
#!/usr/bin/env ruby
# 保持する世代数
retention_generation = 3
require 'aws-sdk-v1'
AWS.config(region: 'ap-northeast-1')
ec2 = AWS::EC2.new
delete_images = ec2.images.with_owner(:self)
.filter("tag:Persistent", 'false')
.sort{ |a,b| b.name <=> a.name }
.drop(retention_generation)
if delete_images.empty?
puts "No AMI will be deleted"
exit 0
end
delete_images.each do |i|
puts "Deletes [#{i.name}]: #{i.id}"
ec2.images[i.id].delete
end
上記のスクリプトをjenkins_toolsディレクトリに作成します。依存性管理のGemfileも上で出てきたものと同様に作成しておきましょう。
Packerで作成したAMIの名前には作成日時が含まれるようにしてあるので、それを新しい方から順番にならべ、保持世代以前のAMIを削除しています。AMI作成時に自動で「Persistent」タグをつけるようにしてあり、これがtrueになっていると、世代管理の対象外となります。そのため、残しておきたい特定のバージョンのAMIにはこれを「true」にしておくとよいでしょう。
あとはこれをJenkinsに先ほどとは別のジョブとして登録するだけです。今回はHookではなく定時実行として登録します。実行頻度はプロジェクトの活発度に応じて変えてよいでしょう。
#!/bin/bash
export PATH="${JENKINS_HOME}/bin:/usr/local/bin:$PATH"
cd jenkins_tools
bundle install
bundle exec ruby purge_old_ami.rb
まとめ
この他にも様々な連携があると思います。もう有名な使い方ですがServerspecと連携してAMIの自動テストは行ったほうがよいでしょう。Packer、非常に便利なツールで導入も簡単なのでまだ使ったことが無い方はぜひお試し下さい。
ちなみに、2015/3/29にクラスメソッド主催のDevelopers.IO 2015 Developer Dayというイベントがあり、私もこのブログで紹介したような事例を含む自動化周りのお話をさせて頂きます。私のセッションの他にもAWSやビッグデータにDeep Diveできるセッションがたくさんありますので、お申込みがまだのかたはぜひご検討下さい!