AWS
EC2
Ansible

AnsibleからEC2インスタンスをいい感じにつくる

はじめに

Ansible上からEC2をたてて見ます。
いい感じにできるようにまとめてみました。

気をつけたところ

EC2は複数台まとめて起動したい事が多いのでそのあたりも意識します。
commandモジュールやshellモジュールはできるだけ使わないようにします。
できるだけ柔軟で簡素で安全な記載にします。

用意しておくもの

以下の情報を控えておきます。
あとで変数定義してAnsible上から使います。

  • 利用するAWSリージョン
  • 利用するAWSの認証方法
    • この記事ではprofileに記載します。
  • keypairの名前
    • keypairを登録していないならしておきます
  • 建てるEC2の要件の確認
  • インスタンスの名前
    • インスタンスタイプ
    • 立ち上げるインスタンスの台数
    • EBSの種類(gp2、iopsなど)、容量
    • OSの種類
    • 利用するサブネットについているNameタグの名前
      • サブネットやVPCを切っていない場合、あらかじめ切っておきます
    • 利用するセキュリティグループの名前
      • セキュリティグループが作られていない場合、あらかじめ作っておきます
    • EIPの要、不要
    • ひもづけるIAM Roleの名前
      • IAM Roleが作られていない場合、あらかじめ作っておきます

手順

変数としてEC2の情報を定義する

Ansibleから上記の情報が利用できるようにします。
Playbookのvarsブロックやvarsファイルの中などに記載すると良いでしょう。

vars/main.yml
# 定義例
aws_profile: sample
region: ap-northeast-1
ec2_ubuntu_version: xenial-16.04
ec2_eip: true
ec2_list:
  - name: sample-instance1
    instance_type: t2.small
    security_group: sample-web-sg-name
    iam_role_name: sample-iam-role-name
    root_volume_size: 8
    subnet_name: sample-1a-public-subnet
  - name: sample-instance2
    instance_type: t2.small
    security_group:
      - sample-web-sg-name
      - sample-admin-sg-name
    iam_role_name: sample-iam-role-name
    root_volume_size: 8
    subnet_name: sample-1c-public-subnet

EC2の起動タスクを記載する

OSの名前からAMIを取得する

この手順は、AWSのコミュニティからAMI(Amazonマシンイメージ)を取得するときに必要です。
AMIを自分で焼いて利用するときなどは不要です。

tasks/main.yml
- name: search ami list
  ec2_ami_facts:
    filters:
      name: "ubuntu/images/hvm-ssd/ubuntu-{{ ec2_ubuntu_version }}-amd64-server-*"
      block-device-mapping.volume-type: gp2
    region: "{{ region }}"
    profile: "{{ aws_profile }}"
  register: list_ami

- name: set latest ami params
  set_fact:
    map_ami_param: "{{ list_ami.images | sort(attribute='creation_date') | last }}"

ec2_ami_factsモジュールはAnsible2.5からの追加となります。
何かしらの理由でAnsible2.5が使えない場合は
ec2_ami_findモジュールを使うとよいでしょう。
 
 
ここでは、gp2タイプのubuntu16.04のOSイメージを所得します。
利用するEBSタイプ、OSのバージョンにあわせて変更するとよいでしょう。
 
この時、取得したAMIリストをソートして最後のイメージを選択しています。
このようにしてAMIのIDを取得することで
その時のコミュニティで公開されている最新のOSイメージを利用することができます。
WebコンソールでEC2を建てるときに表示されるOSイメージを同じように利用できますね。

サブネットの名前からサブネットIDリストを作る

ec2_vpc_subnet_factsモジュールを利用して、サブネットIDを取得します。
ここでは、少し前処理を加えています。

サブネットリストの取得

単一サブネットに配置するならfactを噛ませれば完了です。
が、AZ対応などで配置するサブネットを分けたいときが多いので
まずサブネットの名前リストを取り出し、まとめてfactsにかけます。

tasks/main.yml
- name: find subnet id
  ec2_vpc_subnet_facts:
    region: "{{ region }}"
    profile: "{{ project }}"
    filters:
      "tag:Name": "{{ item }}"
  with_items:
    "{{ ec2_list | map(attribute='subnet_name') | list | unique }}"
  register: map_subnet_id

uniqueフィルターをかけ、多くのインスタンスを同時に起動するとき
問い合わせが多くかからないようにしましょう。

取り出せるように加工

set_factで取り出したサブネットリストを加工します。

tasks/main.yml
- name: make subnet map
  set_fact:
    name_subnet: "{{ item.tags.Name }}"
    id_subnet: "{{ item.subnet_id }}"
  with_items: "{{ map_subnet_id.results | map(attribute='subnets') | list }}"
  register: map_subnets_result

- name: make subnet list
  set_fact:
    list_subnets:
      "{{ map_subnets_result.results | map(attribute='ansible_facts') | list }}"

まず、with_itemsで取り出したリストをループさせつつ
あとで取り出しやすいようにしておきます。
factだけだと最後のループの変数で上書きされてしまうので
registerに登録しておきます。

そして、registerから取り出したものを切り出して
新しい変数(list_subnets)に設定して完了です。

EC2インスタンスの起動

今まで設定した情報を元に、インスタンスを起動します。

tasks/main.yml
- name: create ec2 instance
  ec2:
    image: "{{ map_ami_param.image_id }}"
    instance_profile_name: "{{ iam_role_name }}"
    instance_type: "{{ item.instance_type }}"
    key_name: "{{ item.key_name | default('default-key')}}"
    vpc_subnet_id: "{{ list_subnets | selectattr ('name_subnet', 'equalto', item.subnet_name) | map(attribute='id_subnet') | list | first }}"
    group: "{{ item.security_group }}"
    wait: yes
    volumes:
      - device_name: "/dev/sda1"
        volume_type: "gp2"
        volume_size: "{{ item.root_volume_size }}"
        delete_on_termination: true
    instance_tags:
      Name: "{{ item.name }}"
      app: "{{ project }}"
      env: "{{ env }}"
      role: "{{ item.role }}"
    count_tag:
      Name: "{{ item.name }}"
    exact_count: 1
    profile: "{{ project }}"
    region: "{{ region }}"
  with_items:
    - "{{ ec2_list }}"
  register:
    ec2

- name: add EIP
  ec2_eip:
    profile: "{{ project }}"
    region: "{{ region }}"
    instance_id: "{{ item.tagged_instances.0.id }}"
  with_items: "{{ ec2.results }}"
  when: ec2_allocate_eip is defined and ec2_allocate_eip == True
  register: ec2_eip

本当はec2_instanceモジュールを使った方がいいのですが
ansible 2.5.4だとinstance profileを設定するとバグが出るのでec2モジュールを使用します。
https://github.com/ansible/ansible/pull/37465
ansible 2.6で修正されているので
stable releaseが作られるのを待ちましょう。
 
パラメータが多いので、テーブルで解説します。
 
https://docs.ansible.com/ansible/2.4/ec2_module.html
きちんとしたドキュメントはこちらを参照してください。

パラメータ名 説明
image AMIのImage IDです。上のami_factから取得しています
instance_profile_name attachするIAM Roleの名前です
instance_type インスタンスタイプをしています
key_name 登録済みのkey_pairの名前を設定します
vpc_subnet_id インスタンスを動かすsubnet_idを指定します。上でfactしたlistから取り出しています
group セキュリティグループの名前です。単一文字列か文字列リストで指定します
wait Ansibleが次のtaskに移る前にEC2の起動を待つかどうかです
volumes 起動時にattachするボリュームインスタンスです。リストで指定します(この例ではシステム用につけるボリュームのみ記載します)
volumes.0-9.device_name デバイス名を記載します。以下のURLを参考にしてください
(補足) https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/device_naming.html
volumes.0-9.volume_type volume typeを記載します。EBSのgpsやiopsなどのボリュームタイプを記載します
volumes.0-9.volume_size EBSに割り当てるボリュームの容量を指定します。単位はGByteです
volumes.0-9.delete_on_termination EC2インスタンス削除時にEBSも消すかどうかの設定をします
instance_tags インスタンス起動時に設定するタグを辞書形式で指定します
count_tag 既存のEC2インスタンスを数え、特定のタグ付いているインスタンスが指定数以下ならEC2を立ち上げません。Nameタグと組み合わせて冪とう性を保つ目的によく使われます
exact_count count_tagで数えるインスタンスの指定数です。1にしてPlaybookに記載したインスタンスを1つだけ起動する目的によく使われます

サブネットは、selectattrで特定のサブネットのパラメータに指定して
mapでサブネットIDを取得しています。

おわりに

EC2の構築も、Ansibleの進歩とともに便利になってきました。
体感ですが、3桁台数ぐらいまでならAnsibleが一番楽に利用できるツールと思います。
昔の設定を見なおしてより効率よく管理したいですね。