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ファイルの中などに記載すると良いでしょう。

``` markdown: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を自分で焼いて利用するときなどは条件にあうようにfilterを変更しましょう。。
`owners`パラメータには、amazon、microsoft、もしくは自分の所有しているAWSアカウントのIDを指定しておくと良いでしょう。

``` yaml:tasks/main.yml
- name: search ami list
  ec2_ami_facts:
    filters:
      name: "ubuntu-{{ ec2_ubuntu_version }}-amd64-server-dotnetcore-*"
      block-device-mapping.volume-type: gp2
    region: "{{ region }}"
    owners: amazon
    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を取得することで
その時のAWSで公開されている最新のOSイメージを利用することができます。
WebコンソールでEC2を建てるときに表示されるOSイメージを同じように利用できますね。

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

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


#### サブネットリストの取得

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

``` yaml: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で取り出したサブネットリストを加工します。
2018/10/02追記
Ansible 2.6.5で、戻り値が変わっていました。
どのバージョンで変わったかまでは不明ですが、
新しいバージョンを利用している方は引数の値を変更してください

``` yaml:tasks/main.yml
- name: make subnet map(new ansible version)
  set_fact:
    name_subnet: "{{ item.tags.Name }}"
    id_subnet: "{{ item.subnet_id }}"
  with_items: "{{ map_subnet_id.subnets }}"
  register: map_subnets_result

#- name: make subnet map(old ansible version)
#  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インスタンスの起動

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

``` yaml: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が一番楽に利用できるツールと思います。
昔の設定を見なおしてより効率よく管理したいですね。


## 編集履歴

2018/09/13

コミュニティAMIを利用するとセキュリティリスクがあったため
一部記事を修正しました。

2018/10/01

新しいAnsibleで戻り値が更新されていたので、一部taskを更新しました。