AMI作成のPackerプロジェクトのワタシ的ベストプラクティス!

AMI作成のPackerプロジェクトのワタシ的ベストプラクティス!
目次

様々なプロジェクトで仕事をするにあたって、AWSのAMI(Amazon Machine Image)を多くつくるようになりました。 今回はPackerプロジェクトの個人的なベストプラクティスをまとめました。

作成したリポジトリ

はじめに、作成したリポジトリを以下に晒しておきます。

また、前提条件は以下とします。

  • 作成対象はAMI(Amazon Machine Image)
  • プロビジョニングには PackerAnsible を使う
  • インスタンスのテストには Sererspec を使う

PackerやAnsible、Sererspec自体の解説は割愛します。

Vagrantを使ってローカル環境でデバッグできるようにしておく

Ansible Playbookの書き始めの頃は、可能であればローカル環境上に VagrantVirtual Box を使ってインスタンスを起動して、それに対してプロビジョニングするようにしました。

記述したAnsible PlaybookをいきなりAWS環境上のEC2へプロビジョニングをすることはやめました。 それは、プロビジョニング以外の処理(インスタンスの起動/停止処理)もあって、デバッグに時間がかかるためです。

Vagrantfileの準備

まずは、以下のような Vagrantfile を準備します。とりあえず centos/7 を指定しています。

 1Vagrant.configure("2") do |config|
 2  config.vm.box = "centos/7"
 3
 4  config.vm.network :forwarded_port, id: "ssh", guest: 22, host: 2222
 5
 6  config.vm.provider "virtualbox" do |vb|
 7    vb.memory = "1024"
 8  end
 9  config.vm.provision "shell", inline: <<-SHELL
10    #yum -y update
11    # add user centos
12    useradd centos
13    passwd -f -u centos
14    echo "centos ALL = NOPASSWD: ALL" >> /etc/sudoers.d/centos
15    echo "centos ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/centos
16    # setup SSH
17    mkdir -p /home/centos/.ssh; chown centos /home/centos/.ssh; chmod 700 /home/centos/.ssh
18    cp /home/vagrant/.ssh/authorized_keys /home/centos/.ssh/authorized_keys
19    chown centos /home/centos/.ssh/authorized_keys
20    chmod 600 /home/centos/.ssh/authorized_keys
21    # setup sshd config
22    sed -ri 's/#PermitRootLogin yes/PermitRootLogin yes/g' /etc/ssh/sshd_config
23    sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
24    sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config
25    systemctl restart sshd
26  SHELL
27end

公開されているAMIとVagentの centos/7 には、インストール済みのモジュールや、設定に差分があるため、 可能なかぎり config.vm.provision ブロックで差分を吸収しています。ここはハマると若干時間を取られます。

ここではAnsibleがSSHしてプロビジョニングするための centos ユーザの追加と sudo 権限の追加、sshd への設定を追加しています。

Null BuilderでVagrantに対してプロビジョニングする

vagrant up コマンドを使って自前で起動する場合は ssh コマンドだけできれば良いので、 Null Builder を使います。

Packerのtemplateファイルの builders を抜粋します。

 1"builders":[
 2    {
 3        "type": "null",
 4        "communicator": "ssh",
 5        "ssh_pty": true,
 6        "ssh_timeout" : "1m",
 7        "ssh_host": "{{user `ssh_host`}}",
 8        "ssh_port": "{{user `ssh_port`}}" ,
 9        "ssh_username": "{{user `ssh_user`}}",
10        "ssh_private_key_file": "{{user `ssh_key`}}"
11    }
12]

templateファイル上で展開される変数は以下を与えます。

1{
2    "ssh_host": "127.0.0.1",
3    "ssh_user": "centos",
4    "ssh_key": "../.vagrant/machines/default/virtualbox/private_key",
5    "ssh_port": "2222"
6}

なお、ポート 2222Vagrantfile 内で 22 番と既にポートフォワードする設定を追加済みです。

Vagrantのスナップショット機能を使う

Vagrant はバージョン 1.8 から vagrant snapshot サブコマンドが使用可能になっています。 これは、インスタンスの状態を保存&復元するための機能です。

Ansible Playbook が何度でも流せるように、初期化時に Vagrantインスタンスの状態を一度スナップショットとして取得し、 Packer実行前に毎回 restore するようにします。

Ansibleの後にServerSpecで検査する

Ansible Playbookが実行された後は Serverspec を実行したいですよね。

残念ながら、PakcerにはServerspecのprovisionerがついていないので、 シェルを実行するためのprovisionerを使ってServerspecを実行します。

provisionerは ansible や shell-local を使う

Packerのtemplateファイルで記載するprovisionerには ansibleshell-local を使います。

ansible-localshellを使いません。

理由として、ansible-localshell は Packerでのプロビジョニング時に AnsibleServerspecプロビジョニング先のインスタンスにインストールする必要がでてきてしまうため です。実際のサービスで稼働させるインスタンスに不要なライブラリがインストールされているのは何とも気持ちの悪いものです。

必ず、ローカルマシンやCIサーバといったプロビジョニングする側にモジュールはインストールします。

ここでは rake コマンドをラップした run_spec.sh を呼び出していて、SSHするために必要な情報を引数として渡しています。

 1    "provisioners": [
 2        {
 3            "type": "ansible",
 4            "playbook_file" : "ansible/playbook-{{user `provision_target`}}.yml",
 5            "user": "centos",
 6            "ssh_host_key_file": "{{user `ssh_key`}}",
 7            "ansible_env_vars": [
 8                "ANSIBLE_HOST_KEY_CHECKING=False",
 9                "ANSIBLE_NOCOLOR=True"
10            ]
11        },
12        {
13            "type": "shell-local",
14            "command": "cd serverspec && sh ./run_spec.sh {{user `ssh_host`}} {{user `ssh_port`}} {{user `ssh_key`}} {{user `provision_target`}} {{user `ssh_user`}}"
15        }
16    ]

設定ファイルは、roleごと、環境ごとに準備する

Packerのtemplateファイルでは多くの変数を必要とします。 それらの依存関係を理解して、適切な設定ファイルに分割するのは割と重要だと思っています。

個人的には以下の3種類を作成し、Packer実行時に引数としてこれらの3種類を組合せて渡します。

プラットフォームに依存するPacker template

Packerのtemplateファイルはプロビジョニングするプラットフォーム毎に作成します。 ローカルなのか、AWSなのか、Azureなのか、で1つずつ作るイメージです。

ファイル名用途
ami-aws-template.jsonAWSにプロビジョニングするために使うPacker template
ami-local-template.jsonローカルのVagrantにプロビジョニングするために使うPacker template

環境に依存する設定ファイル

プラットフォームの環境に依存する設定ファイルです。 ここで言う「環境」とは、開発環境、ステージング環境、商用環境のような、システムを管理する単位一式のことを指しています。

ざっくり以下のようなイメージです。

ファイル名用途
env-A-variables.jsonAWSアカウント Aにプロビジョニングするときに使う設定
env-B-variables.jsonAWSアカウント Bにプロビジョニングするときに使う設定
env-local-variables.jsonローカルのVagrantにプロビジョニングするために使う設定
1{
2    "aws_region": "your-region",
3    "aws_vpc_id": "vpc-xxxxxxxxxxxxxx",
4    "aws_subnet_id": "subnet-xxxxxxxxxxx",
5    "ssh_user": "centos",
6    "use_profile": "your-profile",
7    "aws_instance_role": "your-packer-role",
8    "aws_keypair_name": "your-keypair-name"
9}

AWSアカウント単位でVPCのIDやキーペアの情報は変わってくるので、そのような情報はここにもたせます。

Ansible Role(システムコンポーネント)に依存する設定ファイル

ファイル名用途
role-hoge-variables.jsonRole hoge をプロビジョニングするときに使う設定

AnsibleのRole(システムコンポーネント)に依存する設定ファイルです。 ベースとするAMIのインスタンスIDや、作成したAMI名のprefixなど置いておくといいと思います。

1{
2    "packer_tag_prefix":  "Packer-Hoge-AMI",
3    "instance_tag_prefix": "HOGE_OPTIMIZED",
4    "aws_source_ami": "ami-xxxxxxxx"
5}

実行コマンドは別ファイルでラップしておく

一連のPacker実行コマンドの量が増えてしまうため、 コマンドをラップします。 シェルでもなんでもいいですが、私は Makefile にしています。

実行引数は最低限以下の2つで済みます。

  • Ansibleのプロビジョニング対象のRole
  • AnsibleやServerspecがEC2へSSHするためのSSH鍵
 1PACKER = cd packer
 2# AnsibleのRole
 3ROLE = $1
 4# EC2へのSSH鍵(絶対パス)
 5AWS_KEY_FILE = $2
 6
 7# Vagrant初期化時に make init-vagrant を実行します。 initial-saveという名前でスナップショットを保存
 8init-vagrant:
 9	vagrant halt && vagrant destroy -f && vagrant up --provision && \
10		vagrant snapshot save initial-save
11
12# Vagrantに対して先程のNull Builderでプロビジョニングします
13test-local:
14	@${PACKER} && \
15		vagrant snapshot restore initial-save && \
16		packer build -var-file=env-local-variables.json \
17		-var 'ssh_key=$(CURDIR)/.vagrant/machines/default/virtualbox/private_key' \
18		-var 'provision_target=${ROLE}' \
19		ami-local-template.json
20
21# AWSのAMIを作成します
22create-ami:
23	@${PACKER} && \
24		packer build \
25		-var-file=env-aws-variables.json \
26		-var-file=role-${ROLE}-variables.json \
27		-var 'aws_key_file=${AWS_KEY_FILE}' \
28		-var 'provision_target=${ROLE}' \
29		ami-aws-template.json

まとめ

今回はPackerでAMIを作成するときの個人的ベストプラクティスを紹介しました。

  • ローカル環境でVagrantインスタンスを使って、Ansible PlaybookとServerspecの動作確認をする
    • Vagrantfile では config.vm.provision ブロックでAMIとの差分を減らす努力は必要
  • Ansibleの後にServerspecを流す
    • provisoner は ansibleshell-local を使う
  • 変数ファイルは依存関係ごとに分ける
  • 実行コマンドをラップしたファイルを作る