Jenkins + Ansible + PackerでAMI作成を自動化する
最近は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のテンプレートを書きます。
{ "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でタスク化しました。
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も置いておいてあげましょう。
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スクリプトを作っておきました。
#!/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できるセッションがたくさんありますので、お申込みがまだのかたはぜひご検討下さい!