Ansible + Packerで、デプロイと同一のAnsiblePlaybookを使ってAMIを生成する

令和あけましておめでとうございます。山﨑です。
本記事は、「Ansibleによってサーバをデプロイした後、マシンイメージを作成するフローの自動化」の第二回です。
前記事はこちら

blog.engineer.adways.net

はじめに

Amazon EC2 Auto Scaling の導入に伴い、AnsibleとPackerを用いて、最新デプロイを反映したマシンイメージ(AMI)を作成するフローを自動化します。
前記事 では、Ansibleを用いた構成管理についてまとめました。
今回は、このAnsibleで管理される設定を反映したAMIを生成する処理についてまとめます。さらに、Ansibleを用いて、デプロイとAMI生成を連続して実行します。

AMI生成に使用するツールは Packer です。
IaaSや仮想環境にマシンイメージを自動生成してくれるコマンドラインツールなのですが、構成管理ツールと連携したプロビジョニング機能を備えているのが特徴です。
そのため、前記事で作成したAnsibleをそのまま利用して、マシンイメージを作成することができます。
詳細は公式ドキュメント をご参照ください。

目次

  • 環境について
    • バージョン
    • サーバ構成
  • 成果物
    • 動作イメージ
    • ディレクトリ構成
    • 設定について
    • 各ファイルの説明
      • packer-after-playbook.yml
      • ansible_remote/update.yml
      • group_vars/[stage].yml
      • ansible_remote/update.template
      • packer_vars
  • 今後の展望
  • 参考にしたサイト
  • 最後に

環境について (一部再掲)

バージョン

サーバ構成

  • 4種類の異なるロールを持つサーバがあります。
  • サーバ環境は2種類用意されています。

ロール(役割)

- web           # WEBページを管理するサーバ
- worker-001    # バックグラウンド処理を非同期に担うサーバ。worker-002とは異なる役割を担う
- worker-002    # バックグラウンド処理を非同期に担うサーバ。worker-001とは異なる役割を担う
- batch         # バッチ処理を管理するサーバ

ステージ(環境)

- production   # 本番環境
- staging      # ステージング環境

成果物

動作イメージ

サーバへのデプロイでは、下記のansible-playbookコマンドを実行します。
引数にてロールとステージを与えることで、対象のサーバを特定しています。

$ ansible-playbook packer-after-ansible.yml -i production --extra_vars "server_role=web"

このコマンドによって、下記2つのAnsiblePlaybookが実行されます。
- (1) Production環境内の起動済みEC2サーバ: web へのデプロイ
- (2) (1) を反映したマシンイメージ(AMI): ami-production-web の作成、および旧AMIのバックアップ作成

(1) については 前記事 をご参照ください。
本記事では (2) を達成するAnsiblePlaybookについて記述します。

最終的には、8種類のサーバに準じた、8種類のAMIが作成されます。

# production
- ami-production-web
- ami-production-worker-001
- ami-production-worker-002
- ami-production-batch

# staging
- ami-staging-web
- ami-staging-worker-001
- ami-staging-worker-002
- ami-staging-batch

ディレクトリ構成

.
├── ansible.cfg
├── packer-after-playbook.yml  # (1)(2)を達成するAnsiblePlaybook
├── ansible-packer.log         # (1)(2)を達成するpacker-after-playbook.ymlの実行ログ
├── playbooks                  # (1)を達成するAnsiblePlaybook
│   ├── roles
│   ├── web.yml
│   ├── worker.yml
│   ├── worker-001.yml
│   ├── worker-002.yml
│   └── batch.yml
├── group_vars
│   ├── all
│   ├── web
│   ├── worker
│   ├── worker-001
│   ├── worker-002
│   ├── batch
│   ├── production
│   └── staging
├── ssh_config                 # (1)用のSSH接続設定ファイル
├── production                 # (1)用のインベントリファイル
├── staging                    # (1)用のインベントリファイル
└── ansible_remote
     ├── update.template       # (2)を達成するPackerの設定ファイル
     ├── update.yml            # (2)を達成するAnsiblePlaybook
     └── packer_vars           # (2)を達成するPacker用の変数ファイル
         ├── common.json
         ├── production.json
         ├── staging.json
         ├── test.json
         ├── web.yml
         ├── worker.json
         └── batch.yml

設定について

概念図

f:id:AdwaysEngineerBlog:20190510113005p:plain

ポイント
  • SSH接続について
    • (1)では、インベントリファイルが用いられます。接続設定は ssh_config または 〜/.ssh/config に設定しています。
    • (2)では、Packer - Buildersの設定が用いられます。(1)で使用されたインベントリファイルは参照されません。
  • 動的な環境設定は、すべてグループで管理します。
    • 前記事 にて定義したグループを、Packerでも用います。
    • group_varsを参照するため、インベントリディレクトリ(inventory_dir)にgroup_varsと同階層を指定するようにします。
  • サーバごとに固定のAMI名を定義し、定義されたAMI名を持つAMIを、最新と定義しています。
    • これにより、後続処理にて最新のAMIを検索する機会が削減されました。
    • バックアップは別名称で保持します。
    • AMI名は、グループ名から動的に生成されるようにしました。

以上を踏まえ、各ファイルについて説明していきます。

各ファイルの説明

packer-after-playbook.yml

---
# (1) デプロイ
- import_playbook: "playbooks/{{ server_role }}.yml"

# (2) AMI作成
- hosts: localhost
  tasks:
    - name: execute create or update ami with packer
      become: false
      local_action: >
        shell ansible-playbook
        ansible_remote/packer.yml -i {{ inventory_file }}
        --extra-vars "server_role={{ server_role }}"
        -vvv >>ansible-packer.log
  • (2)はhosts: localhostで実行します。インベントリファイルへの設定は不要です。
  • (2)では、マジック変数 inventory_file を用いて、(1)で用いたインベントリファイルを指定しています。これは、後続処理で用いるマジック変数 inventory_dir を有効にするためです。

ansible_remote/update.yml

---
- hosts: localhost
  gather_facts: false   # 速度軽減
  vars:
    ami_prefix: ami-  # packerの`ami_prefix`と同じにする
    ami_name: "{{ ami_prefix }}{{ stage }}-{{ server_role }}"  # packerの`ami_name`と同じにする
  tasks:
    # (a) 既存AMIのバックアップを取得
    ## AMI名で検索し、一致するAMIのAMIIDを取得する
    ## ※ローカルのプロファイル設定に依存するので注意
    - name: get ami ImageId
      shell: "aws ec2 describe-images --owners self --filters 'Name=name,Values={{ ami_name }}' | jq -r '.Images[].ImageId'"
      register: source_ami_image_id

    ## 既存AMIイメージをコピーし、リネームして保存
    - name: copy and rename source ami
      shell: >
        aws ec2 copy-image
        --name {{ ami_name }}-$(date '+%FT%H-%m-%S')
        --source-region ap-northeast-1
        --source-image-id {{ source_ami_image_id.stdout }}
      when: source_ami_image_id.stdout
    
    # (b) 最新AMI作成
    - name: "[packer build] update ami from existing image or create ami from amzn2 public image"
      command: >
        packer build
        -var-file={{ inventory_dir }}/ansible_remote/packer_vars/common.json
        -var-file={{ inventory_dir }}/ansible_remote/packer_vars/{{ stage }}.json
        -var-file={{ inventory_dir }}/ansible_remote/packer_vars/{{ server_role }}.json
        -var='inventory_dir={{ inventory_dir }}'
        {{ inventory_dir }}/ansible_remote/update.template
  • hosts: localhostで実行します。
  • 既存AMIをソースAMIとして用いるため、コピーしてリネームすることでバックアップとします。
  • マジック変数 inventory_dir には、(1)と同一のパスが格納されています。そのため、このplaybookはgroup_varsを参照することができます。
  • packer buildに与えるグループ名は、下記に定義したものを参照しています。
    • server_role: packer-after-playbook.yml実行時に与えたオプション--extra_vars
    • stage: インベントリファイル内の変数stage

group_vars/[stage].yml

group_vars/production.yml

 ---
 stage: production

...
  • ステージ名を持つstage変数を定義しています。

前記事 参照

update.template

{
  "builders" : [{
    "type" : "amazon-ebs",
    "access_key": "{{ user `aws_access_key` }}",
    "secret_key": "{{ user `aws_secret_key` }}",
    "region" : "ap-northeast-1",
    "vpc_id": "{{ user `vpc_id` }}",
    "subnet_id": "{{ user `subnet_id` }}",
    "security_group_id": "{{ user `security_group_id` }}",
    "associate_public_ip_address" : "true",
    "force_delete_snapshot" : true,
    "force_deregister" : true,
    "instance_type" : "{{ user `instance_type` }}",
    "source_ami_filter": {
      "filters": {
        "name": "{{ user `ami_prefix` }}{{ user `stage` }}-{{ user `server_role` }}"
      },
      "owners": ["999999999999"],
      "most_recent": true
    },
    "launch_block_device_mappings": [{
      "delete_on_termination": true,
      "device_name": "/dev/sdb",
      "volume_size": "{{ user `volume_size` }}",
      "volume_type": "gp2"
    }],
    "ssh_username": "ec2-user",
    "ssh_timeout": "5m",
    "ami_name" : "{{ user `ami_prefix` }}{{ user `stage` }}-{{ user `server_role` }}"
  }],

  "provisioners" : [{
    "type" : "ansible",
    "playbook_file" : "{{ user `inventory_dir` }}/playbooks/{{ user `server_role` }}.yml",
    "inventory_directory": "{{ user `inventory_dir` }}",
    "groups": ["{{ user `server_role` }}", "{{ user `stage` }}"],
    "extra_arguments" : "-vvvv"
  }]
}
  • SSH接続設定が(1)とは異なることに注意します。
    • Template内に、接続先のVPC、サブネット、セキュリティグループを定義します。
    • ローカルから接続するため、接続先はパブリックサブネットである必要があります。
  • ソースAMIと同一の名称でAMIを作成します。
    • force_deregisterオプションは、AMI名が重複した場合、重複するAMIを削除します。このオプションによって、既存AMIを擬似的に上書きすることができます。
      • AMI名は同一ですが、AMIIDが変わることに注意が必要です。
    • ソースとする既存AMIが存在しない場合、Amazon公式イメージから新規作成することもできます。AmazonLinux2の場合、下記のように設定します。
    "source_ami_filter": {
      "filters": {
        "name": "amzn2-ami-hvm-*-x86_64-gp2"
      },
      "owners": ["137112412989"],
      "most_recent": true
    },
  • PackerのプロビジョニングにてAnsibleを実行する際は、group_varsが参照されるようにします。
    • プロビジョニングに使用される一時的なインベントリファイルは、デフォルトではシステム固有の一時ファイル置き場に置かれてしまうので、group_varsが参照できなくなってしまいます。
      inventory_directoryオプションにルートディレクトリを指定します。(1)で利用したinventory_dirを引数で与えることで、(1)と同一のAnsible構成を利用することができます。
    • (1)で作成したインベントリファイルが参照されないため、グループは未定義の状態です。デプロイに必要なグループをgroupsオプションに与えます。

packer_vars

Packerで用いる動的な変数も、前記事で定義したAnsibleのグループで管理できるようにしました。
(1)で参照したグループをそのまま(2)に与え、変数はグループ単位でファイルにまとめて読み込むことで、管理を容易にしています。

ansible_remote/update.ymlより引用

    - name: "[packer build] update ami from existing image or create ami from amzn2 public image"
      command: >
        packer build
        -var-file={{ inventory_dir }}/ansible_remote/packer_vars/common.json
        -var-file={{ inventory_dir }}/ansible_remote/packer_vars/{{ stage }}.json
        -var-file={{ inventory_dir }}/ansible_remote/packer_vars/{{ server_role }}.json
        -var='inventory_dir={{ inventory_dir }}'
        {{ inventory_dir }}/ansible_remote/update.template

各変数ファイルを見ていきます。

common.json

共通設定ファイルです。
AMI名のプリフィックスや、AWSクレデンシャル情報など、すべての環境において用いる変数を定義しています。

{
  "ami_prefix" : "ami-",
  "//": "AWSアクセスキー: IAM Roleを使用する選択肢もあります",
  "aws_access_key": "<aws_access_key>",
  "aws_secret_key": "<aws_secret_key>"
}
[stage].json

ステージごとの設定ファイルです。 VPCやサブネット、セキュリティグループなど、ステージ(環境)の設定を定義しています。

production.json
{
  "stage": "production",
  "vpc_id": "vpc-a1b2c3d4e5f6g7h8i9j0",
  "subnet_id": "subnet-a1b2c3d4e5f6g7h8i9j0",
  "security_group_id":"sg-a1b2c3d4e5f6g7h8i9j0"
}
[server_role].json

「ロール」ごとの設定ファイルです。
インスタンスタイプやアタッチするボリュームのサイズなど、インスタンスの設定を定義しています。

web.json
{
  "server_role" : "web",
  "instance_type" : "t2.micro",
  "volume_size" : "1"
}
worker-001.json
{
  "server_role" : "worker-001",
  "instance_type" : "r5.large",
  "volume_size" : "10"
}

今後の展望

  • 現状、後処理として下記を手動実行しており、このままではまだ完全なる自動化とは言い難いところです。
    ansible_remote/update.ymlに下記の処理を追加したいです。
    • AutoScalingの起動テンプレートに、作成した最新のAMIを設定する処理
    • どんどん溜まっていくAMIバックアップの過去分を削除する処理
    • AMI削除と同時にスナップショットも削除する処理
  • AWSのクレデンシャルをベタ書きで管理しているため、IAMロールを利用したいです。

参考にしたサイト

下記サイトを大変参考にさせていただきました。ありがとうございます!

Packer+AnsibleによるAMIの作成
PackerのAnsibleProvisionerでインベントリ毎のgroup_varsを使い分ける

最後に

この記事の作成に多大にご協力いただいた足立さん、AnsibleやPackerについて随時情報を提供してくださった植垣さん、Ansibleのレビューをしてくださった奥村さん、本当にありがとうございました。