AnsibleでAWS環境を自動構築する

## はじめに

Ansibleで環境構築の自動化に凝っています。

AWSの環境構築も含めて全て自動化できたらもっと楽になるんじゃないかなーと思って、
Ansibleの標準モジュールでAWS環境構築ができるので試してみました。

この記事で作成したplaybookはgithubで公開しています。

https://github.com/rednes/ansible-aws-sample
（AWS01フォルダ）

## AWSの何を作るか

以下の内容をAnsibleで自動構築できるようにします。

* VPCの作成
* Internet Gatewayの作成
* サブネットの作成
* ルートテーブルの作成
* セキュリティグループの作成
* EC2インスタンスの作成
* EC2インスタンスにWordPress環境の構築

## 前提

* ansible, botoをインストールすること
* AWSのサーバー構築に使用するIAMユーザが作成されていること
* AWSマネジメントコンソールでキーペア登録していること


## AWS構成図

今回Ansibleで構築するAWSの構成図はこんな感じです。

![AWS構成図](https://raw.githubusercontent.com/rednes/ansible-aws-sample/img/img/aws01.png)


## ディレクトリ構成

```ディレクトリ構成
├── ansible.cfg
├── host_vars
│   └── localhost.yml
├── hosts
│   ├── ec2.ini
│   └── ec2.py
├── roles
│   ├── ec2
│   │   ├── defaults
│   │   │   └── main.yml
│   │   └── tasks
│   │       ├── ec2.yml
│   │       ├── main.yml
│   │       ├── security_group.yml
│   │       └── vpc.yml
│   ├── wordpress
│   │   ├── defaults
│   │   │   └── main.yml
│   │   └── tasks
│   │       └── main.yml
│   └── yum
│       ├── defaults
│       │   └── main.yml
│       └── tasks
│           └── main.yml
└── site.yml
```

## inventory

AWSリソース関連モジュールはすべてlocalhostで実行するのですが、
EC2インスタンスは作成するまでどのIPになるかわかりません。
そのためhostsフォルダのec2.ini,ec2.pyを利用して動的インベントリを使用しています。

詳細は以下のサイトを参考にしてください。

* [Ansible Dynamic Inventoryを使ってAWSのEC2インスタンスに付与されたタグでプロビジョニング対象を決める](https://qiita.com/NewGyu/items/ae8e68a248694ae84ee0)
* [Dynamic Inventory &mdash; Ansible Documentation](http://docs.ansible.com/ansible/latest/intro_dynamic_inventory.html)


以下の様に、site.ymlでEC2インスタンス作成後にRefresh inventoryメタタスクを定義しています。
このメタタスクを実行することでEC2インスタンス作成後に動的インベントリの再取得が実行され、
作成後のEC2インスタンスに対して環境構築を継続することが可能になります。

```yaml:site.yml
---
- name: create Amazon Web Services
  hosts: localhost
  connection: local
  gather_facts: False
  roles:
    - ec2
  tasks:
    - name: Refresh inventory
      meta: refresh_inventory

- name: build wordpress
  hosts: tag_Name_WordPress
  user: ec2-user
  become: True
  gather_facts: true
  roles:
    - yum
    - wordpress
```

## Playbookの解説

AWS関連は全てec2のroleで作成しています。

```yaml:roles/ec2/tasks/main.yml
---
- include_tasks: vpc.yml
- include_tasks: security_group.yml
- include_tasks: ec2.yml
```

vpc.ymlでVPCからルートテーブルまで、
security_group.ymlでセキュリティグループ、
ec2.ymlでec2インスタンスを作成しています。

### 1. VPCの作成

ec2_vpc_netモジュールでVPCを構築します。
VPC名とCIDRブロックを指定してVPCを定義します。
作成したVPCの情報は変数vpc_netに格納してInternetGateway作成時等にVPC IDを利用します。

Ansibleは冪等性を確保しており、新規に作成すべきか既に作成されているかをVPC名で識別しています。
そのため、2回目の実行を行った場合、同名のVPCが既に存在することになるので作成されません。

```yaml:roles/ec2/tasks/vpc.ymlの一部
- name: VPC作成
  ec2_vpc_net:
    name: "{{ aws.vpc.name }}"
    cidr_block: "{{ aws.vpc.cidr_block }}"
    region: "{{ aws.common.region }}"
    tags: "{{ aws.vpc.tags }}"
    tenancy: default
  register: vpc_net
```

```yaml:roles/ec2/defaults/main.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    name: AnsibleVPC
    cidr_block: 10.0.0.0/16
    tags:
      Name: AnsibleVPC
```

### 2. Internet Gatewayの作成&VPCにアタッチ

ec2_vpc_igwモジュールでInternet Gatewayを構築します。
VPC IDでアタッチするVPCを定義します。
VPC IDはVPC作成時の結果変数(vpc_net)に含まれているのでこれを利用します。

```yaml:roles/ec2/tasks/vpc.ymlの一部
- name: Internet Gateway作成
  ec2_vpc_igw:
    region: "{{ aws.common.region }}"
    vpc_id: "{{ vpc_net.vpc.id }}"
    tags: "{{ aws.vpc.igw.tags }}"
    state: present
  register: vpc_igw
```

```yaml:roles/ec2/defaults/main.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    igw:
      tags:
        Name: AnsibleIGW
```

### 3. サブネットを作成

ec2_vpc_subnetモジュールでサブネットを構築します。
サブネットではVPC, Availability Zone, CIDRを指定します。

今回は１つのサブネットしか作成していませんが、
サブネットは複数作成することが想定されるため、
with_dict構文を使用することで変数定義のリスト要素追加で複数サブネットに対応できるようにしています。

```yaml:roles/ec2/tasks/vpc.ymlの一部
- name: subnet作成
  ec2_vpc_subnet:
    region: "{{ aws.common.region }}"
    state: present
    vpc_id: "{{ vpc_net.vpc.id }}"
    az: "{{ aws.common.region }}{{ item.value.zone }}"
    cidr: "{{ item.value.cidr }}"
    map_public: "{{ item.value.map_public|default(True) }}"
    tags: "{{ item.value.tags }}"
  with_dict: "{{ aws.vpc.subnet }}"
  register: vpc_subnet
```

```yaml:roles/ec2/defaults/main.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    subnet:
      subnet1:
        tags:
          Name: public-a
        cidr: 10.0.1.0/24
        zone: a
```

### 4. ルートテーブルを設定

ec2_vpc_route_tableモジュールでルートテーブルを構築します。
ルートテーブルでは主に対象となるサブネットとInternetGatewayと内向けのCIDRを指定します。

subnetsに対象とするサブネットをSubnet ID,Subnet名またはCIDRで定義します。
今回は、サブネット作成時の結果変数(vpc_subnet)からSubnet IDを指定しています。

routesでルートの定義をしています。
任意のIP全て(0.0.0.0/0)に対してはInternetGatewayの方向に流れるようにして、
VPC内部(10.0.0.0/16)に対してはlocal側に流れるように定義しています。

```yaml:roles/ec2/tasks/vpc.ymlの一部
- name: route table作成
  ec2_vpc_route_table:
    vpc_id: "{{ vpc_net.vpc.id }}"
    region: "{{ aws.common.region }}"
    tags: "{{ aws.vpc.route_table.tags }}"
    subnets: "{{ vpc_subnet.results | map(attribute='subnet.id') | list }}"
    routes:
      - dest: 0.0.0.0/0
        gateway_id: "{{ vpc_igw.gateway_id }}"
      - dest: "{{ vpc_net.vpc.cidr_block }}"
        gateway_id: local
  register: route_table
```

```yaml:roles/ec2/defaults/main.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    name: AnsibleVPC
    cidr_block: 10.0.0.0/16
    tags:
      Name: AnsibleVPC
    route_table:
      tags:
        Name: public-route-table
```

### 5. セキュリティグループを作成

ec2_groupモジュールでセキュリティグループを構築します。
セキュリティグループでは主にVPCとインバウンドルールを指定します。

VPC IDを取得するためにec2_vpc_net_factsモジュールを実行して、
VPC名からVPC IDを取得しています。

```yaml:roles/ec2/tasks/security_group.yml
---
- name: vpc_id取得
  ec2_vpc_net_facts:
    region: "{{ aws.common.region }}"
    filters:
      "tag:Name": "{{ aws.vpc.name }}"
  register: vpc_net_fact
  check_mode: no

- name: security group作成
  ec2_group:
    name: "{{ aws.vpc.security_group.name }}"
    description: "{{ aws.vpc.security_group.description }}"
    tags:
      Name: "{{ aws.vpc.security_group.name }}"
    vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
    region: "{{ aws.common.region }}"
    purge_rules: "{{ aws.vpc.security_group.purge_rules|default(False) }}"
    rules: "{{ aws.vpc.security_group.rules }}"
  register: security_group
```

```yaml:roles/ec2/defaults/main.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    name: AnsibleVPC
    cidr_block: 10.0.0.0/16
    tags:
      Name: AnsibleVPC
    security_group:
      name: AnsibleWeb
      description: EC2 group
      rules:
        - proto: tcp
          ports:
            - 22
          cidr_ip: 0.0.0.0/0
        - proto: tcp
          ports:
            - 80
            - 443
          cidr_ip: 0.0.0.0/0
```

### 6. EC2の作成＆セキュリティグループの設定

ec2モジュールでEC2インスタンスを構築します。
VPCやサブネット等ネットワーク周りを*_factsモジュールを利用して、
名称からIDを引っ張ってきて定義しています。

キーペア名だけはhost_vars/localhost.ymlに定義を分けているので、
自分の環境にあわせて書き換えてください。

インスタンスの作成にはある程度時間がかかるため、
wait_forモジュールを利用してssh接続できるようになるまでWaitを入れるようにしています。

```yaml:roles/ec2/tasks/ec2.yml
---
- name: security group取得
  ec2_group_facts:
    region: "{{ aws.common.region }}"
    filters:
      "tag:Name": "{{ aws.vpc.security_group.name }}"
  register: ec2_group_facts
  check_mode: no

- name: subnet取得
  ec2_vpc_subnet_facts:
    region: "{{ aws.common.region }}"
    filters:
      "tag:Name": "{{ aws.vpc.subnet.subnet1.tags.Name }}"
  register: ec2_subnet_facts
  check_mode: no

- name: Provision a set of instance
  ec2:
    key_name: "{{ my_vars.ec2.key_name }}"
    group_id: "{{ ec2_group_facts.security_groups[0].group_id }}"
    vpc_subnet_id: "{{ ec2_subnet_facts.subnets[0].id }}"
    assign_public_ip: yes
    instance_type: "{{ aws.vpc.ec2.instance_type }}"
    region: "{{ aws.common.region }}"
    image: "{{ aws.vpc.ec2.image }}"
    wait: yes
    wait_timeout: 300
    count_tag:
      Name: "{{ aws.vpc.ec2.name }}"
    exact_count: 1
    instance_tags:
      Name: "{{ aws.vpc.ec2.name }}"
  register: ec2

- name: wait for ssh
  wait_for:
    host: '{{ item.public_ip }}'
    port: 22
    timeout: 300
  with_items: '{{ ec2.tagged_instances }}'
```

```yaml:roles/ec2/defaults/main.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    subnet:
      subnet1:
        tags:
          Name: public-a
        cidr: 10.0.1.0/24
        zone: a
    security_group:
      name: AnsibleWeb
    ec2:
      name: WordPress
      instance_type: t2.micro
      image: ami-2a69be4c
```

```yaml:host_vars/localhost.yml
---
my_vars:
  ec2:
     key_name: XXXX # AWSに登録したキーペア名を入力
```

### その後の構築

作成したEC2インスタンスに対してyum,wordpressロールで、
WordPress環境を構築していますが詳細は割愛します。

playbookの内容読んだらだいたいわかると思います。

## まとめ

AnsibleでAWSの環境構築を自動化できるようPlaybookを作成してみましたが、
yaml形式でまとめることでとても見通しが良くなっていると思います。

まだAWS純正のCloudFormationは使ったことないのでどちらが管理しやすいか比較ができていないので、そのうちCloudFormationも使って比較してみたいと思います。