Ansible の ec2 モジュールでインスタンスを起動してそのままタスクを続行

Ansible の ec2 モジュールを使えば Ansible で ec2 インスタンスを起動することができますが、起動するインスタンスの Public IP は起動してみなければわからないので、同じプレイブックで通常のタスクをそのまま続行することはできません。

ec2 モジュールの結果を `set_fact` でホストの `ansible_ssh_host` 変数に登録してやればそのまま ssh で接続してタスクを続行することができます。

## インベントリ

次のようなインベントリファイルを使います。変数は必要に応じて `group_vars` や `host_vars` に分けると良いです。

```ini:ec2.ini
################################################################################
# Role groups

[web]
web01
web02

[ap]
ap01
ap02

################################################################################
# Subnet groups

[zone-a]
ap01
web01

[zone-c]
ap02
web02

################################################################################
# EC2 groups

[ec2:children]
ap
web

################################################################################
# Default vars

[all:vars]
image=ami-4985b048 # Amazon Linux AMI 2014.09.1 (HVM)
tag_env=prod

################################################################################
# Role vars

[ap:vars]
tag_role=ap
security_group=sg-12345678

[web:vars]
tag_role=web
security_group=sg-xxxxxxxx

################################################################################
# Subnet vars

[zone-a:vars]
vpc_subnet_id=subnet-12345678

[zone-c:vars]
vpc_subnet_id=subnet-xxxxxxxx
```

合計４台のインスタンスを起動します。

ap と web が２台ずつで、Availability Zone を別々にするために Subnet も指定します。

## プレイブック

次のようなプレイブックを使います。

```yaml:site.yaml
---
- hosts: all
  gather_facts: no
  connection: local
  tasks:
    - name: create ec2 instances
      register: ec2
      ec2:
        region: ap-northeast-1
        key_name: oreore
        instance_type: t2.micro
        image: "{{ image }}"
        vpc_subnet_id: "{{ vpc_subnet_id }}"
        group: "{{ security_group }}"
        assign_public_ip: yes
        instance_profile_name: ec2-prod
        wait: yes
        wait_timeout: 300
        exact_count: 1
        count_tag:
          Name: "{{ inventory_hostname }}"
          Env: "{{ tag_env }}"
          Role: "{{ tag_role }}"
        instance_tags:
          Name: "{{ inventory_hostname }}"
          Env: "{{ tag_env }}"
          Role: "{{ tag_role }}"

    - name: set instances vars
      set_fact:
        ec2_instance: "{{ ec2.tagged_instances[0] }}"

    - name: wait for ssh
      wait_for: host="{{ ec2_instance.public_ip }}" port=22

- hosts: all
  gather_facts: no
  remote_user: ec2-user
  sudo: yes
  vars:
    ansible_ssh_host: "{{ ec2_instance.public_ip }}"
  tasks:
    - shell: uname -a
      register: result
      changed_when: no

    - debug: var=result.stdout
```

前半がブートストラッピングで、後半がプロビジョニング（コンフィグレーション？）です。

プロビジョニングは仮に `uname -a` を実行するだけにしています（なので実質なにもしていない）。

## ansible.cfg

起動したばかりのインスタンスのホスト鍵は不明なので、カレントディレクトリに `ansible.cfg` を作成してホスト鍵のチェックを無効にします。

```ini:ansible.cfg
[defaults]
host_key_checking = False
```

## 実行

プレイブックを実行すると ec2 インスタンスを起動した後に ssh 経由で接続してプロビジョニングできます（この例では uname -a コマンドをシェルモジュールで実行しているだけですが）。

```console
$ ansible-playbook -i ec2.ini site.yaml

PLAY [all] ********************************************************************

TASK: [create ec2 instances] **************************************************
changed: [ap02]
changed: [ap01]
changed: [web01]
changed: [web02]

TASK: [set instances vars] ****************************************************
ok: [web02]
ok: [web01]
ok: [ap02]
ok: [ap01]

TASK: [wait for ssh] **********************************************************
ok: [ap02]
ok: [ap01]
ok: [web01]
ok: [web02]

PLAY [all] ********************************************************************

TASK: [shell uname -a] ********************************************************
ok: [web01]
ok: [ap02]
ok: [ap01]
ok: [web02]

TASK: [debug var=result.stdout] ***********************************************
ok: [web01] => {
    "result.stdout": "Linux ip-10-0-1-244 3.14.20-20.44.amzn1.x86_64 #1 SMP Mon Oct 6 22:52:46 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux"
}
ok: [ap01] => {
    "result.stdout": "Linux ip-10-0-1-123 3.14.20-20.44.amzn1.x86_64 #1 SMP Mon Oct 6 22:52:46 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux"
}
ok: [web02] => {
    "result.stdout": "Linux ip-10-0-3-158 3.14.20-20.44.amzn1.x86_64 #1 SMP Mon Oct 6 22:52:46 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux"
}
ok: [ap02] => {
    "result.stdout": "Linux ip-10-0-3-186 3.14.20-20.44.amzn1.x86_64 #1 SMP Mon Oct 6 22:52:46 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux"
}

PLAY RECAP ********************************************************************
ap01                       : ok=5    changed=1    unreachable=0    failed=0
ap02                       : ok=5    changed=1    unreachable=0    failed=0
web01                      : ok=5    changed=1    unreachable=0    failed=0
web02                      : ok=5    changed=1    unreachable=0    failed=0
```

## プレイブックの内容

プレイブックの内容をざっくり説明します。

----

```yaml
- hosts: all
  gather_facts: no
  connection: local
```

ec2 モジュールはローカルで実行する必要があるので `connection: local` を指定します。

----

```yaml
    - name: create ec2 instances
      register: ec2
      ec2:
```

ec2 モジュールでインスタンスを起動してその結果を ec2 変数に入れます。

----

```yaml
        wait: yes
        wait_timeout: 300
```

インスタンスが起動するまで待機します。

----

```yaml
        exact_count: 1
        count_tag:
          Name: "{{ inventory_hostname }}"
          Env: "{{ tag_env }}"
          Role: "{{ tag_role }}"
        instance_tags:
          Name: "{{ inventory_hostname }}"
          Env: "{{ tag_env }}"
          Role: "{{ tag_role }}"
```

`instance_tags` はインスタンスに付与するタグです。

`exact_count` は起動するインスタンスの数です。`count_tag` に指定されたタグが付与されたインスタンスが既に存在する場合、その分は起動済と判断されます（なので２回実行してもインスタンスが追加で起動することはありません）。

なお、今回の使い方だとインベントリで記述したホスト１つに付きインスタンスも１つでなければならないので `exact_count` に 1 以外の値を指定することはできません（そうしないで 2 個目以降のインスタンスでプロビジョニングのタスクが実行されません）。

----

```yaml
    - name: set instances vars
      set_fact:
        ec2_instance: "{{ ec2.tagged_instances[0] }}"
```

ec2 モジュールが取得したインスタンスの情報を `ec2_instance` 変数に登録します。`ec2.instances[0]` にも同じ値が入っていますが、インスタンスが起動しなかったとき（既に同じタグのインスタンスが起動済のとき）は空になるので `ec2.tagged_instances[0]` を使います。

----

```yaml
    - name: wait for ssh
      wait_for: host="{{ ec2_instance.public_ip }}" port=22
```

上の方で `wait: yes` でインスタンスが起動するまで待機していますが、インスタンスが起動しても sshd が起動していなければ後続のタスクを実行することができないので、インスタンスの 22 番ポートが利用可能になるまで待機します。

----

```yaml
- hosts: all
  gather_facts: no
  remote_user: ec2-user
  sudo: yes
```

これ以降のタスクは ssh でインスタンスに接続して実行するので `connection` は指定しません。

----

```yaml
  vars:
    ansible_ssh_host: "{{ ec2_instance.public_ip }}"
```

ssh で接続できるようにするためにインスタンスの Public IP を `ansible_ssh_host` 変数に設定します。

## さいごに

１つのプレイブックでインスタンスの起動からプロビジョニングまでできましたが、以下のような方法の方が使いやすい気がするのであまりメリットは無いかもしれません。

1. ec2 モジュールで `user_data` を指定して clout-init でプロビジョニング
    - 大抵の場合は独自の AMI から起動すると思うので clout-init で十分
    - `user_data` は `group_vars` や `host_vars` で書くと良い
2. インスタンス起動のプレイブックとプロビジョニングのプレイブックを分ける
    - 静的なインベントリで ec2 インスタンスを起動
    - ダイナミックインベントリでインスタンスの情報を拾ってきてプロビジョニング