Applibotでのサーバイメージ作成 – Packer+Ansible -(AWS AMI編)

こんにちは。

インフラチームです。

弊社ではAWS/GCPを使用しており、それぞれのサーバ構築/環境設定はAnsibleで行なっています。

環境の設定の更新を行う場合、AWSであればAMI、GCPであればマシンイメージを更新しておく必要があります。

ゴールデンイメージ(起動してからの設定更新を行わない、最新設定のイメージ)で管理しているのですが、

そのイメージを更新しようとする場合、手動対応では下記手順が必要になります。

Packer_ない時.png

一つ一つは大したことない作業ですが、毎回この手順では手間も待ち時間も多く、ミスが発生しやすかったです。

起動対象のイメージを間違えたり、

他の作業している間に起動したことすら忘れてたり、

イメージ取得後のインスタンス消し忘れなんかもよくありました。

この手順を管理し、実行してくれるのがpackerになります。

AMI/GCE image作成の工数を大幅に削減できておりますので、

それぞれどのように継続的に回せるようにしているかをお話しさせていただきます。

今回はAWS(AMI)での設定になります。

packerとは

https://www.packer.io/

パプリッククラウドのマシンイメージ、VMイメージ、コンテナイメージまで作成をサポートするツール

安心のhashicorp製

弊社ではAWS,GCPイメージ作成に使用しています。

dockerイメージも作って見たのですが、playbookを同じように入れてしまうと、コンテナイメージが複雑かつ重いものになってしまいます。

role単位で独立して作成すれば良いのですが、従来のplaybook管理と異なってしまうこともあり現在は使用しておりません。

packerがやってくれる事

  1. AMIからインスタンスを起動
  2. 起動したインスタンスにAnsibleを適用
  3. AMIを作成
  4. 1で起動したインスタンスを削除

※post-processorは今回使用していない為省いています。

Packer動作.png

継続的に回すために

こちらを毎回指定する必要があります。

  • どのAMIから起動させるか( 1. )
  • どのplaybookを適用するか( 2. )

もちろん毎回ベタで書くわけにもいかないのでルールとスクリプト化を行い対応しています。

1. どのAMIから起動させるか

ここではどのAMIからインスタンスを起動するかを決めておく必要があります。

AMIの運用の仕方次第ではありますが、弊社では下記のようにルールを設定しています。

AMI運用ルール

  • 名前の統一。versionは作成日時で管理。
    • ex: hoge-prd-api-base-20180301-1234
  • 3世代管理。同じ種類のAMIは3代前までは残し、それ以上古いのは削除。
  • 使用するAMIは常に最新の日付のものとする。

packerで起動するインスタンスは最新の日付のAMIと決めています。

最新の日付-時間のAMI IDを取得しpackerのconfigに追加します。

2. どのplaybookを適用するか

どのplaybookを適用するか、というのもルールを設けています。

各環境毎にplaybookを用意(dev環境=dev.yml,本番環境=prd.ymlという感じ)

どの環境のどのカテゴリ(APIや管理画面用等)のAMIを作成するか、という値をparameter.jsonから読み込めるようにしています。

Config

parameter.json/packer.jsonを同じディレクトリにおきます。

それぞれ下記用途で分けています。

  • parameter.json : 変数定義
  • packer.json : 動作定義

parameter.json( sample_parameter.template )

{
    "account": "__ACCOUNT__",
    "ami_name": "__AMINAME__",
    "source_ami": "__SOURCEAMI__",
    "volume_size": "__VOLUMESIZE__",
    "ansible_enviroment": "__ENVIROMENT__",
    "ansible_group": "__GROUP__",
    "ansible_category": "__CATEGORY__",
    "playbook_directory" : "__PLAYBOOKDIR__"
}

AMI ID、AMI NAME、適用するansibleのplaybook名等、

対象が毎回変化する値を入れるようにしています。

sample_parameter.templateファイルを用意し、

create_ami.shでこのファイルに値を入れた、parameter.jsonを生成します。

packer.json

{
  "builders" : [{
    "type" : "amazon-ebs",
    "profile" : "{{user `account`}}",
    "region" : "ap-northeast-1",
    "instance_type" : "t2.nano",
    "source_ami" : "{{user `source_ami`}}",
    "ssh_username" : "ec2-user",
    "ssh_timeout" : "5m",
    "ami_name" : "{{user `ami_name`}}",
    "ami_block_device_mappings": [
      {
        "device_name": "/dev/xvda",
        "volume_type": "gp2",
        "volume_size": "{{user `volume_size`}}",
        "delete_on_termination": true
      }
    ]
  }],
  "provisioners" : [
      {
        "type" : "ansible",
        "extra_arguments": [
            "--tags","{{user `ansible_group`}}",
            "--extra-vars","group={{user `ansible_group`}}",
            "--extra-vars","category={{user `ansible_category`}}",
            "--vault-password-file","{{user `playbook_directory`}}/vault.txt"
        ],
        "sftp_command": "/usr/libexec/openssh/sftp-server -e",
        "playbook_file" : "{{user `playbook_directory`}}/playbooks/{{user `ansible_enviroment`}}.yml"
      }
  ]
}

packerの動作定義ファイル

シンプルにbuildersとprovisionersだけで構成しています。

  • builders
    • どこでイメージを作るか、ベースとなるインスタンスはどのような設定で起動させるか、の定義
      • AWSのBuilder設定、必要なIAM権限についてはこちらを参照
      • profileを使用して~/.aws/credentialsからキーを読み込む設定としています。
    • ssh設定
  • provisioners
    • インスタンス起動後に適用する操作定義
      • type : ansibleで実行環境からansibleを適用する、という設定。

ansibleでのプロビジョニングについて

provisionersにtype:ansibleとして記述いたします。

https://www.packer.io/docs/provisioners/ansible.html

どのように動作するか

まずconfigと実行コマンドを見比べて見ます。

packerは実行時の動作を標準出力で吐いてくれる為、追いやすいです。

      {
        "type" : "ansible",
        "extra_arguments": [
            "--tags","{{user `ansible_group`}}",
            "--extra-vars","group={{user `ansible_group`}}",
            "--extra-vars","category={{user `ansible_category`}}",
            "--vault-password-file","{{user `playbook_directory`}}/vault.txt"
        ],
        "sftp_command": "/usr/libexec/openssh/sftp-server -e",
        "playbook_file" : "{{user `playbook_directory`}}/playbooks/{{user `ansible_enviroment`}}.yml"
      }

↑ 設定

↓ コマンド

※ 実行ログから抜粋
==> amazon-ebs: Executing Ansible: ansible-playbook --extra-vars packer_build_name=amazon-ebs packer_builder_type=amazon-ebs -i /tmp/packer-provisioner-ansible243167728 hoge_playbooks/playbooks/dev.yml -e ansible_ssh_private_key_file=/tmp/ansible-key776005013 --tags dev-api --extra-vars group=dev-api --extra-vars category=env --vault-password-file hoge_playbooks/vault.txt

↓見やすく
ansible-playbook
    --extra-vars packer_build_name=amazon-ebs packer_builder_type=amazon-ebs
    -i /tmp/packer-provisioner-ansible243167728
    hoge_playbooks/playbooks/dev.yml
    -e ansible_ssh_private_key_file=/tmp/ansible-key776005013
    --tags dev-api
    --extra-vars group=dev-api
    --extra-vars category=api
    --vault-password-file hoge_playbooks/vault.txt

指定した”extra_arguments”のオプション以外にも、何行かあるのがわかります。

ssh

起動したインスタンスに対してsshでコネクションのテストを行います(builders)

どのように対象を指定しているのかを理解する必要があります。

Packer: builders → provisioners流れ

Packer_ssh動作.png

この2行がsshに関わってきます。

    -i /tmp/packer-provisioner-ansible243167728
    -e ansible_ssh_private_key_file=/tmp/ansible-key776005013
  • ポート
    • 実行環境からsshできるようにしているか確認
    • セキュリティグループを指定していない場合は22ポートが全開放のグループがアタッチされる
  • キー
    • こちらも特に指定していなければpackerで作成したキーが毎回登録されます
      • ex: -e ansible_ssh_private_key_file=/tmp/ansible-key776005013
    • sshのキーペアを実行環境内で作成
    • AWSへキーペア登録
      • 最後までpackerを実行すれば削除されますが、途中で止めると「packer-***」なキーが残り続ける
      • 登録したキーペアを起動するインスタンスに設定
        • authorized_keysにたまり続けるので、削除の仕組みは入れておきたい
  • 対象ホスト(inventry file)
    • buildersで起動したインスタンスのIPが記述されている、実行時に作成したもの
      • ex: -i /tmp/packer-provisioner-ansible243167728
    • hostsのgroup指定が効かない。playbookでhostsを指定している場合は対象が取れないと判断されるので注意。

playbook例

NG

hostsの対象不明となる

- hosts: dev-api
  user: ec2-user
  become: yes
  vars_files:
    - group_vars/dev.yml
  roles:
    - common
    - nginx
    - td-agent

OK

hostsはallとし、対象はtagsで指定

- hosts: all
  user: ec2-user
  become: yes
  vars_files:
    - group_vars/dev.yml
  roles:
    - common
    - nginx
    - td-agent
  tags:
    - dev-api
使用しているオプションについて
  • extra_arguments
    • ansible-playbookコマンド実行時につけるオプションを記述
    • playbook内でansible-vaultで暗号化している箇所がある為、”–vault-password-file”で”vault.txt”からパスワードを読み込み実行している
  • “sftp_command”: “/usr/libexec/openssh/sftp-server -e”
    • Ansibleがファイルの転送に使用するSFTPプロトコルを処理するためにPackerによってプロビジョニングされているマシン上で実行するコマンド
    • デフォルトが”/usr/lib/sftp-server -e.”だが、弊社で使用しているAmazonLinux/CentOS6上ではパスが合わない為指定

イメージ作成の流れ(AWS)

では実行に入ります。

最終的にpacker buildを叩くのですが、対象のAMIの取得等があります。

一貫して実行する為にスクリプトファイルとしました。

create_ami.sh

parameter.jsonの作成とpackerの実行(エラー処理なし・・・すいません)

VOLUME_SIZEだけベタ書きというのも修正しないとです。

#!/bin/bash

PLAYBOOK_DIR=`dirname ${0}`

# パラメータ設定
ACCOUNT="$1"
PLAYBOOK_ENV="$2"
PLAYBOOK_CATEGORY="$3"
AMI_PREFIX="${ACCOUNT}-${PLAYBOOK_ENV}-${PLAYBOOK_CATEGORY}-base"

# PLAYBOOK GROUP
PLAYBOOK_GROUP="${PLAYBOOK_ENV}-${PLAYBOOK_CATEGORY}"

# 元となるAMIの情報を取得
SOURCEAMI_INFO=`aws ec2 describe-images 
    --profile "${ACCOUNT}" 
    --owners self | 
    jq -r '.Images | sort_by(.Name)| .[] | .Name + " " + .ImageId' | 
    grep "${AMI_PREFIX}" | 
    tail -1`
SOURCEAMI_NAME=`echo ${SOURCEAMI_INFO} | awk '{print $1}'`
SOURCEAMI_ID=`echo ${SOURCEAMI_INFO} | awk '{print $2}'`

echo "SOURCEAMI_NAME : ${SOURCEAMI_NAME}
SOURCEAMI_ID : ${SOURCEAMI_ID}"


# AMIパラメータ設定
AMI_NAME="${AMI_PREFIX}-`date +'%Y%m%d-%H%M'`"
VOLUME_SIZE="50"


# parameterファイルを作成
cat ${PLAYBOOK_DIR}/sample_parameter.template | 
    sed -e "s/__ACCOUNT__/${ACCOUNT}/g" 
        -e "s/__AMINAME__/${AMI_NAME}/g" 
        -e "s/__SOURCEAMI__/${SOURCEAMI_ID}/g" 
        -e "s/__VOLUMESIZE__/${VOLUME_SIZE}/g" 
        -e "s/__ENVIROMENT__/${PLAYBOOK_ENV}/g" 
        -e "s/__GROUP__/${PLAYBOOK_GROUP}/g" 
        -e "s/__CATEGORY__/${PLAYBOOK_CATEGORY}/g" 
        -e "s@__PLAYBOOKDIR__@${PLAYBOOK_DIR}@g" 
        > ${PLAYBOOK_DIR}/parameter.json

cat ${PLAYBOOK_DIR}/parameter.json

# build
packer build 
    -var-file ${PLAYBOOK_DIR}/parameter.json 
    ${PLAYBOOK_DIR}/packer.json
ディレクトリ構成
$ ~/hoge_playbooks
tree -L 2
.
├── create_ami.sh
├── packer.json
├── parameter.json
├── playbooks
│   ├── dev.yml
│   ├── group_vars
│   ├── prd.yml
│   ├── roles (ansible roles)
│   └── stg.yml
├── sample_parameter.template
└── vault.txt

同じディレクトリに

  • packer.json
  • parameter.json
  • Ansible Playbook
  • create_ami.sh
  • vault.txt

を置く。

vault.txtにはansible-vaultのパスワードを記載。

実行
sh create_ami.sh hoge dev api

対象の環境を指定し実行致します。
ACCOUNT="$1"
PLAYBOOK_ENV="$2"
PLAYBOOK_CATEGORY="$3"

# AMI名/対象playbookの選択に使用
AMI_PREFIX="${ACCOUNT}-${PLAYBOOK_ENV}-${PLAYBOOK_CATEGORY}-base"
AMI_NAME="${AMI_PREFIX}-`date +'%Y%m%d-%H%M'`"
# PLAYBOOK --tagsに使う
PLAYBOOK_GROUP="${PLAYBOOK_ENV}-${PLAYBOOK_CATEGORY}"

実行環境

EC2でrundeckサーバを立てており、JOBとして実行しています。

rundeckで実行するメリットは下記になります。

  • 作業環境の統一
  • 実行手順の統一
  • 実行履歴の保存

実行履歴が残るので、どこで失敗したか、いつ実行したかを遡って確認できるのは助かります。
実行フローは下記になります。

イメージ作成フロー(AMI).png

application version 用途
rundeck 2.6.0 JOBサーバ
awscli 1.14.42 AWS操作CLI
packer 1.2.2 イメージ作成
ansible 2.3.0.0 構成管理

packerは最新(2018/4/18現在)で書いてますが、長いこと0.10.2で動いていましたので、最新でなくても問題はありません。

JOBはそれぞれ単体でも動くようにし、連携させたJOBを実行。

  • CreateAMI
    • playbookをpullし、packerを実行するjob
  • CreateLaunchConfig
    • 指定したAMIのLaunchConfigを作成し、AutoScalingGroupへ追加する

最新のAMI作成からAutoScalingGroupへの適用まで実行される為、変更の反映をとても楽にできています。

新しい設定に切り替えるにはBlueGreenデプロイで、サーバを入れ替えれば完了になります。

まとめ

AMIをpacker + ansibleで作成する為にやっていることを書かせていただきました。

もともとansibleで構成管理をしていた中に、packerのサイクルを入れられたことで、イメージ作成は格段に楽になっています。

AMIの管理方法、ansibleのロール管理等もそうですが、ベストプラクティスを参考にしつつ、使いやすい形は常に模索中です。

柔軟に変化できるように今のベースを皆で把握しながら変えてけるようにするということを心がけています。

次回はGoogleComputeEngineのイメージ作成についてお話しさせていただきます。