話題の記事

Jenkins + Ansible + PackerでAMI作成を自動化する

2015.03.20

この記事は公開されてから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が自動で作られるのです。簡単ですね!

Build_Atlassian_Config__Jenkins_

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

Purge_Old_AMIs_Config__Jenkins_

まとめ

この他にも様々な連携があると思います。もう有名な使い方ですがServerspecと連携してAMIの自動テストは行ったほうがよいでしょう。Packer、非常に便利なツールで導入も簡単なのでまだ使ったことが無い方はぜひお試し下さい。

ちなみに、2015/3/29にクラスメソッド主催のDevelopers.IO 2015 Developer Dayというイベントがあり、私もこのブログで紹介したような事例を含む自動化周りのお話をさせて頂きます。私のセッションの他にもAWSやビッグデータにDeep Diveできるセッションがたくさんありますので、お申込みがまだのかたはぜひご検討下さい!