Terraformで立てたec2インスタンスをAnsibleで管理してみる

この記事はニフティグループ [Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nifty)の13日目の記事です。
昨日は@hmmrjnさんの「WebページもOSのDark Modeに対応できる」でした。
個人的にDark Modeは大好きなので、対応がしやすくなるのはとても嬉しいですね。

---

# はじめに
近年、TerraformのようなオーケストレーションツールやChef・Ansibleといった構成管理ツールの利用により、リソースの構築・管理がより容易になってきています。
私も今更ながらAnsibleを扱い始めました。そのなかで、クラウドリソースはIPアドレスが決まっていないことが多いため、インベントリの扱いが非常に面倒なことがありました。

この記事では、Terraformで立てたec2インスタンスに対して、Ansibleで用途ごとにグループ分けして構成管理することを目標としています。
これを達成する手段の一つとして、AnsibleのInventory Pluginを利用してみました。

# 環境
* **OS**： macOS Catalina 10.15.1
* **Ansible**： 2.9.2
* **Terraform**： v0.12.18

# ディレクトリ構成
今回は簡単化のため、以下に示す最小限のディレクトリ構成で進めていきます。
実際に利用する際は各ツールのベストプラクティスに従ってください。

```
.
├── ansible
│   ├── group_vars
│   │   └── aws_ec2.yml
│   ├── inventory
│   │   └── aws_ec2.yml
│   └── site.yml
└── terraform
    ├── ec2.tf
    ├── main.tf
    └── vpc.tf
```

# 前準備
Ansibleで管理する対象サーバーをTerraformで作成します。

## tfファイル作成
今回はタグのみが異なる３つのec2インスタンスを作成するように記述します。
各ec2インスタンスに対するタグの付け方は以下のようになっています。

* タグなし
* Nameタグ： "web-ec2"、Webserverタグ： "httpd"
* Nameタグ： "web-ec2"、Webserverタグ： "nginx"

このタグの付け方がAnsibleのインベントリを作成する上で重要になってきます。

<details><summary>terraform/main.tfの内容</summary><div>

```:main.tf
provider "aws" {
  version = "~> 2.41"
  region  = "ap-northeast-1"
}
```

</div></details>
<details><summary>terraform/ec2.tfの内容</summary><div>

```:ec2.tf
resource "aws_security_group" "web_sg" {
  vpc_id          = aws_vpc.example.id
  ingress {
    from_port     = 22
    to_port       = 22
    protocol      = "tcp"
    cidr_blocks   = ["0.0.0.0/0"]
  }
  ingress {
    from_port     = 80
    to_port       = 80
    protocol      = "tcp"
    cidr_blocks   = ["0.0.0.0/0"]
  }
  egress {
    from_port     = 0
    to_port       = 0
    protocol      = "-1"
    cidr_blocks   = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "example" {
  ami                    = "ami-068a6cefc24c301d2"
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  key_name               = "my-aws-key"
  vpc_security_group_ids = [aws_security_group.web_sg.id]
}

resource "aws_instance" "httpd" {
  ami                    = "ami-068a6cefc24c301d2"
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  key_name               = "my-aws-key"
  vpc_security_group_ids = [aws_security_group.web_sg.id]
  tags = {
    Name      = "web-ec2"
    Webserver = "httpd" 
  }
}

resource "aws_instance" "nginx" {
  ami                    = "ami-068a6cefc24c301d2"
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  key_name               = "my-aws-key"
  vpc_security_group_ids = [aws_security_group.web_sg.id]
  tags = {
    Name      = "web-ec2"
    Webserver = "nginx" 
  }
}
```

</div></details>

<details><summary>terraform/vpc.tfの内容</summary><div>

```:terraform/vpc.tf
resource "aws_vpc" "example" {
  cidr_block                       = "10.0.0.0/16"
  enable_dns_hostnames             = true
  enable_dns_support               = true
}

resource "aws_subnet" "public" {
  vpc_id                           = aws_vpc.example.id
  cidr_block                       = "10.0.0.0/24"
  map_public_ip_on_launch          = true
  availability_zone                = "ap-northeast-1a"
}

resource "aws_internet_gateway" "example" {
  vpc_id                           = aws_vpc.example.id
}

resource "aws_route_table" "public" {
  vpc_id                           = aws_vpc.example.id
}

resource "aws_route" "public" {
  route_table_id                   = aws_route_table.public.id
  gateway_id                       = aws_internet_gateway.example.id
  destination_cidr_block           = "0.0.0.0/0"
}

resource "aws_route_table_association" "public" {
  subnet_id                        = aws_subnet.public.id
  route_table_id                   = aws_route_table.public.id
}

```

</div></details>

## リソース作成
`terraform plan`で確認後、間違えがなければ`terraform apply`を実行します。

```terminal
$ cd terraform
$ terraform init
$ terraform plan
$ terraform apply
```

# インベントリ作成
Ansibleでサーバを管理するためには、各サーバのIPアドレスを知らなければいけません。しかし、ec2インスタンスのようなクラウドリソースは作成するたびに、IPアドレスが変化します。また、WEBサーバやDBサーバといった用途ごとに使い分ける場合、グループ分けをする必要があります。これらを手動でインベントリに記述していくことは大変だと思います。

そこで、Dynamic InventoryやInventory Pluginを利用することで、取得した情報から動的にインベントリを作成することができます。今回はInventory Pluginのうち、AWS EC2用プラグインを利用します。

[Inventory Plugins — Ansible Documentation](https://docs.ansible.com/ansible/latest/plugins/inventory.html)
[aws_ec2 – EC2 inventory source](https://docs.ansible.com/ansible/latest/plugins/inventory/aws_ec2.html)

## aws_ec2 プラグイン
aws_ec2プラグインにより、AWS上にあるec2インスタンスに対するインベントリをコンパイルできます。
以下のようにファイルを記述することで、各ec2インスタンスをタグやインスタンスタイプ等でグループ分けできます。
ここで注意が必要なのは、ファイル名のサフィックスが`aws_ec2.(yml|yaml)`である必要があります。
プラグインを利用したファイルは通常のインベントリファイルと同様に`ansible-playbook`などで扱うことができます。

```yaml:ansible/inventory/aws_ec2.yml
plugin: aws_ec2
regions:
  - ap-northeast-1
strict: no
keyed_groups:
  # Nameタグの値により、グループを分類。グループ名のプレフィックスに"tag_Name_"を付与
  - key: tags.Name
    prefix: tag_Name_
    separator: ""
  # インスタンスタイプ(t2.microなど)により、グループを分類
  - key: instance_type
    prefix: instance_type
  # Webserverタグの値により、グループを分類
  - key: tags.Webserver
    separator: ""
groups:
  # Nameタグのプレフィックスが"web-"のとき、webグループに分類
  web: "tags.Name is match('^web-')"
# ホストごとの変数の設定。
# ここではグローバル経由でアクセスするため、`ansible_host`変数にグローバルIPを格納する。
compose:
  ansible_host: global_ip_address
```

## インベントリの確認
`ansible-inventory`コマンドを利用することでプラグインから作成されたグループ構成を確認することができます。

```terminal
$ ansible-inventory -i inventory/aws_ec2.yml --graph
@all:
  |--@aws_ec2:
  |  |--ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com
  |  |--ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com
  |  |--ec2-52-68-129-109.ap-northeast-1.compute.amazonaws.com
  |--@httpd:
  |  |--ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com
  |--@instance_type_t2_micro:
  |  |--ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com
  |  |--ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com
  |  |--ec2-52-68-129-109.ap-northeast-1.compute.amazonaws.com
  |--@nginx:
  |  |--ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com
  |--@tag_Name_web_ec2:
  |  |--ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com
  |  |--ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com
  |--@ungrouped:
  |--@web:
  |  |--ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com
  |  |--ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com
```
設定したタグやインスタンスタイプごとに、グループ分けができていることがわかります。

# Playbookの実行
最後に、以下のPlaybookを実行して、グループごとに構成管理できるかを確認します。
httpdグループにapache、nginxグループにnginxをインストールして起動します。（このようなグループ分けはあまりなさそうですが、例のために行います）

```yml:ansible/site.yml
---
- hosts: httpd
  gather_facts: no
  become: yes
  tasks:
    - yum:
        name: httpd
        state: latest
    - systemd:
        name: httpd
        state: started

- hosts: nginx
  gather_facts: no
  become: yes
  tasks:
    - command: "amazon-linux-extras enable nginx1.12"
      changed_when: false
    - yum:
        name: nginx
        enablerepo: amzn2extra-nginx1.12
        state: present
    - systemd:
        name: nginx
        state: started
```

```terminal
$ cd ansible
$ ansible-playbook -i inventory/aws_ec2.yml site.yml
PLAY [httpd] *********************************************************************************************************************************************

TASK [Install httpd] *************************************************************************************************************************************
changed: [ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com]

TASK [Start httpd] ***************************************************************************************************************************************
changed: [ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com]

PLAY [nginx] *********************************************************************************************************************************************

TASK [command] *******************************************************************************************************************************************
ok: [ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com]

TASK [yum] ***********************************************************************************************************************************************
changed: [ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com]

TASK [systemd] *******************************************************************************************************************************************
changed: [ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com]

PLAY RECAP ***********************************************************************************************************************************************
ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```
`curl`コマンドでサーバにアクセスしてみます。
それぞれapacheとnginxがインストールできていることがわかります。

```
$ curl -I ec2-13-113-228-112.ap-northeast-1.compute.amazonaws.com
HTTP/1.1 403 Forbidden
Date: Fri, 13 Dec 2019 14:30:29 GMT
Server: Apache/2.4.41 ()
Upgrade: h2,h2c
Connection: Upgrade
Last-Modified: Tue, 22 Oct 2019 22:56:48 GMT
ETag: "e2e-59587b710ac00"
Accept-Ranges: bytes
Content-Length: 3630
Content-Type: text/html; charset=UTF-8
$ curl -I ec2-52-199-119-151.ap-northeast-1.compute.amazonaws.com 
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 13 Dec 2019 14:31:13 GMT
Content-Type: text/html
Content-Length: 3520
Last-Modified: Wed, 28 Aug 2019 19:52:13 GMT
Connection: keep-alive
ETag: "5d66db6d-dc0"
Accept-Ranges: bytes
```

# おわりに
Inventory Pluginにより、Terraformで立てたec2サーバをAnsibleで管理することができました。予めリソースに対するタグの付け方をルール化することで、より良い構成管理ができると思います。

検証機や勉強会用サーバといった同一のサーバを立てたり、クラウド間の移行など、構成管理をしておくことは非常に重要だと思います。そのため、さらにベターなリソース管理を目指して頑張っていきたいです。

# 参考文献
* [packer と terraform と ansible でインフラを作る](https://qiita.com/kanagi/items/6af87a3a59e70e7e06db)
* [Terraform と Ansible で勉強会用の環境を自動構築してみた話](https://tech-lab.sios.jp/archives/13251)