Ansibleで、AWS profileを使ってアカウントごとのInventoryを簡単に生成する


Ansibleで、Dynamic Inventory(EC2 external inventory script)を利用して、AWSアカウントごとのInventoryを作成してみます。

# まずはじめに

## Inventoryファイルとは

Ansibleでは、対象のサーバーをInventoryで管理できます。

ですが、Cloud(AWSとかGoogle Cloud Platformなど)の様なインフラの状態が動的なケースの場合、静的に運用するのはしんどいです。

そこで、Dynamic Inventoryという機能を利用してInventoryを設定することができます。

http://docs.ansible.com/intro_inventory.html
http://docs.ansible.com/intro_dynamic_inventory.html


実際に、Dynamic Inventoryを利用するまでの手順や動きは以下を参照していただくのが良いかと思います。

http://dev.classmethod.jp/cloud/aws/ansible-dynamic-inventory-intro/

## EC2 external inventory scriptとは

ansibleが提供している、Inventory作成用のスクリプトのことです。

このスクリプトは、内部的にpythonのbotoパッケージを利用しており、
`ec2.py`と`ec2.ini`の2ファイルで構成されています。

`ec2.ini`の中身を確認すると、Inventoryの作成するための注意が書いてあります。

一部を抜き出してみました。

```
Generates inventory that Ansible can understand by making API request to
AWS EC2 using the Boto library.

NOTE: This script assumes Ansible is being executed where the environment
variables needed for Boto have already been set:
    export AWS_ACCESS_KEY_ID='AK123'
    export AWS_SECRET_ACCESS_KEY='abc123'
```

要約すると、

> Inventory作るのに、Botoを利用してAWSのAPIを叩きに行くから、AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYが環境変数に設定してある必要があるよ。

という感じです。


## 複数AWSアカウントを切り替える

複数のAWSアカウントを運用している場合、この環境変数を切り替えるのに逐一AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY をexportし直すのは、若干面倒です。(おぼえてられないし)

そこで、aws-cliのprofileという仕組みを使うとユーザーフレンドリーになります。

そもそも、aws-cliがインストールされていないという方は、以下を参考にインストールしてください。
http://docs.aws.amazon.com/cli/latest/userguide/installing.html


aws-cliでは、cliの設定ファイル(configファイル, credentialsファイル)を作ることができます。

その仕組みを使うと、profileという形でAWS環境へアクセスするための設定を記述しておくことができ、複数アカウントへのアクセスも簡単に切り替えることができます。

設定ファイルの作り方は、以下のコマンドで作成できます。

```:command
aws configure [--profile profile-name]
```

EC2 external inventory scriptが使うBotoパッケージでは、credentialsを読み込むのにAWS_PROFILEという環境変数を利用します。(aws-cliでは通常AWS_DEFAULT_PROFILEなどの環境変数を利用するので、ややこしいですね)

BotoのConfigについて詳しくは、ドキュメントを参照してください。
http://boto.readthedocs.org/en/latest/boto_config_tut.html

複数のAWSアカウントへのアクセスは、profileを指定したり、環境変数に設定しておくことで簡単に切り替えることできます。


# 以上を組み合わせてやってみる

今回は、手元のmacでansibleを動かして、AWSのInvetntoryを作るケースでやります。

## 準備する

### Dynamic Inventoryの設定

まずは、さくっとDynamic Inventoryの設定をします。

1. EC2 external inventory scriptを取得する
2. 実行権限を付与する
3. iniファイルへのパスを通す

gistはここにおいておきました。
https://gist.github.com/9c24f23571c6e4d3a545.git

```bash:command
curl -s "https://raw.githubusercontent.com/ansible/ansible/devel/plugins/inventory/ec2.{ini,py}" -o /tmp/ec2.#1
chmod +x /tmp/ec2.py
cd /tmp
export EC2_INI_PATH=/tmp/ec2.ini
```

### AWSのクレデンシャルを設定する


次に、AWSのクレデンシャルを設定します。

環境変数に設定してもいいですが複数のアクセスキーの切り替えが不便なので、上記の通りaws configureで作成した設定ファイルに記述しておきます。

aws configureの詳細は以下から確認できます。

http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html


### ec2.iniを修正する

最後に、ec2.iniを修正します。

EC2 external inventory scriptでは、EC2のAPI callの結果をファイルキャッシュします。
キャッシュのパスは、ec2.iniで設定できます。デフォルトでは、`~/.ansible/tmp`です。

キャッシュ時間は300秒ですが、0秒に設定することで無効にすることが可能です。

```bash:sed
sed -i s/cache_max_age\ =\ 300/cache_max_age\ =\ 0/g /tmp/ec2.ini
```

## 実行してみる

ec2.py実行時に、AWS_PROFILE環境変数を設定します。
すると、aws-cliで`--profile`と指定するような効果が得られます。

```bash:command
AWS_PROFILE=test ./ec2.py --list
```

こんなかんじで、Inventoryが作成できました！

```json
{
  "_meta": {
    "hostvars": {
      "54.65.132.252": {
        "ec2__in_monitoring_element": false, 
        "ec2_ami_launch_index": "0", 
        "ec2_architecture": "x86_64", 
        "ec2_client_token": "1d0072ba-53cf-4f9e-a426-d5dc3ed61943", 
        "ec2_dns_name": "ec2-54-65-132-252.ap-northeast-1.compute.amazonaws.com", 
        "ec2_ebs_optimized": false, 
        "ec2_eventsSet": "", 
        "ec2_group_name": "", 
        "ec2_hypervisor": "xen", 
        "ec2_id": "i-ffadafe6", 
        "ec2_image_id": "ami-4985b048", 
        "ec2_instanceLifecycle": "spot", 
        "ec2_instance_profile": "", 
        "ec2_instance_type": "m3.medium", 
        "ec2_ip_address": "54.65.132.252", 
        "ec2_item": "", 
        "ec2_kernel": "", 
        "ec2_key_name": "mytestkeyname", 
        "ec2_launch_time": "2015-01-14T15:03:54.000Z", 
        "ec2_monitored": false, 
        "ec2_monitoring": "", 
        "ec2_monitoring_state": "disabled", 
        "ec2_persistent": false, 
        "ec2_placement": "ap-northeast-1b", 
        "ec2_platform": "", 
        "ec2_previous_state": "", 
        "ec2_previous_state_code": 0, 
        "ec2_private_dns_name": "ip-10-0-0-129.ap-northeast-1.compute.internal", 
        "ec2_private_ip_address": "10.0.0.129", 
        "ec2_public_dns_name": "ec2-54-65-132-252.ap-northeast-1.compute.amazonaws.com", 
        "ec2_ramdisk": "", 
        "ec2_reason": "", 
        "ec2_region": "ap-northeast-1", 
        "ec2_requester_id": "", 
        "ec2_root_device_name": "/dev/xvda", 
        "ec2_root_device_type": "ebs", 
        "ec2_security_group_ids": "sg-63dac20f", 
        "ec2_security_group_names": "default", 
        "ec2_sourceDestCheck": "true", 
        "ec2_spot_instance_request_id": "sir-02shxw5b", 
        "ec2_state": "running", 
        "ec2_state_code": 16, 
        "ec2_state_reason": "", 
        "ec2_subnet_id": "subnet-2812f840", 
        "ec2_virtualization_type": "hvm", 
        "ec2_vpc_id": "vpc-5312f94b"
      }
    }
  }, 
  "ap-northeast-1": [
    "54.65.132.252"
  ], 
  "ap-northeast-1b": [
    "54.65.132.252"
  ], 
  "ec2": [
    "54.65.132.252"
  ], 
  "i-ffadafe6": [
    "54.65.132.252"
  ], 
  "key_mytestkeyname": [
    "54.65.132.252"
  ], 
  "security_group_default": [
    "54.65.132.252"
  ], 
  "tag_Name_HelloWorld": [
    "54.65.132.252"
  ], 
  "type_m3_medium": [
    "54.65.132.252"
  ], 
  "vpc_id_vpc-5312f83b": [
    "54.65.132.252"
  ]
}
```

privateなIPで取りたい場合は、ec2.iniのdestination_variableとvpc_destination_variableの設定を変更します。

```ini:ec2.iniから抜粋
# This is the normal destination variable to use. If you are running Ansible    
# from outside EC2, then 'public_dns_name' makes the most sense. If you are     
# running Ansible from within EC2, then perhaps you want to use the internal    
# address, and should set this to 'private_dns_name'.                           
#destination_variable = public_dns_name                                         
destination_variable = private_dns_name                                         

# For server inside a VPC, using DNS names may not make sense. When an instance 
# has 'subnet_id' set, this variable is used. If the subnet is public, setting  
# this to 'ip_address' will return the public IP address. For instances in a    
# private subnet, this should be set to 'private_ip_address', and Ansible must  
# be run from with EC2.                                                         
#vpc_destination_variable = ip_address                                          
vpc_destination_variable = private_ip_address  

```

するとこんなかんじで返ってきます。

```json
{
  "_meta": {
    "hostvars": {
      "10.0.0.168": {
        "ec2__in_monitoring_element": false, 
        "ec2_ami_launch_index": "0", 
        "ec2_architecture": "x86_64", 
        "ec2_client_token": "tGPHR1421366125895", 
        "ec2_dns_name": "", 
        "ec2_ebs_optimized": false, 
        "ec2_eventsSet": "", 
        "ec2_group_name": "", 
        "ec2_hypervisor": "xen", 
        "ec2_id": "i-d50e02cc", 
        "ec2_image_id": "ami-4985b048", 
        "ec2_instance_type": "t2.micro", 
        "ec2_ip_address": "", 
        "ec2_item": "", 
        "ec2_kernel": "", 
        "ec2_key_name": "foobar", 
        "ec2_launch_time": "2015-01-15T23:55:26.000Z", 
        "ec2_monitored": false, 
        "ec2_monitoring": "", 
        "ec2_monitoring_state": "disabled", 
        "ec2_persistent": false, 
        "ec2_placement": "ap-northeast-1b", 
        "ec2_platform": "", 
        "ec2_previous_state": "", 
        "ec2_previous_state_code": 0, 
        "ec2_private_dns_name": "ip-10-0-0-168.ap-northeast-1.compute.internal", 
        "ec2_private_ip_address": "10.0.0.168", 
        "ec2_public_dns_name": "", 
        "ec2_ramdisk": "", 
        "ec2_reason": "", 
        "ec2_region": "ap-northeast-1", 
        "ec2_requester_id": "", 
        "ec2_root_device_name": "/dev/xvda", 
        "ec2_root_device_type": "ebs", 
        "ec2_security_group_ids": "sg-6e628301", 
        "ec2_security_group_names": "base", 
        "ec2_sourceDestCheck": "true", 
        "ec2_spot_instance_request_id": "", 
        "ec2_state": "running", 
        "ec2_state_code": 16, 
        "ec2_state_reason": "", 
        "ec2_subnet_id": "subnet-2812f840", 
        "ec2_tag_Foo": "Bar", 
        "ec2_tag_Name": "Blog", 
        "ec2_virtualization_type": "hvm", 
        "ec2_vpc_id": "vpc-5312f83b"
      }
    }
  }, 
  "ap-northeast-1": [
    "10.0.0.168"
  ], 
  "ap-northeast-1b": [
    "10.0.0.168"
  ], 
  "ec2": [
    "10.0.0.168"
  ], 
  "i-d50e02cc": [
    "10.0.0.168"
  ], 
  "key_foobar": [
    "10.0.0.168"
  ], 
  "security_group_base": [
    "10.0.0.168"
  ], 
  "tag_Foo_Bar": [
    "10.0.0.168"
  ], 
  "tag_Name_Blog": [
    "10.0.0.168"
  ], 
  "type_t2_micro": [
    "10.0.0.168"
  ], 
  "vpc_id_vpc-5312f83b": [
    "10.0.0.168"
  ]
}
```


# まとめ

あとは、AWS_PROFILEの値を切り替えれば、アカウントごとのInverntoryを簡単に取得することができます。

Inventoryファイルはキャシュディレクトリに出力されていますが、上書きしていくので複数アカウントのInventoryを全てファイルとして保持したい場合は、teeコマンドなどで出力しておきましょう。

こんなかんじで、まとめて取得できますね。


```bash:create_inventory_files.sh
PROFILES=( "your_profile_foo" "your_profile_bar" )
for profile in ${PROFILES[@]}
do
  AWS_PROFILE=$profile ./ec2.py --list | tee /tmp/$profile.json
done
```

gistはここに置きました
https://gist.github.com/szkkentaro/e9f4295687a6ef9a728e

何かのお役に立てば幸いです。


# その他
##  トラブルシューティング

### Botoがインストールされていない

botoが見つからない場合があります

```:terminal
my-mba-2% ./ec2.py --list                 
Traceback (most recent call last):
  File "./ec2.py", line 121, in <module>
    import boto
ImportError: No module named boto
```

そんな時はインストールしましょう

```:terminal
my-mba-2% sudo pip install boto
Password:
Downloading/unpacking boto
  Downloading boto-2.35.1-py2.py3-none-any.whl (1.3MB): 1.3MB downloaded
Installing collected packages: boto
Successfully installed boto
Cleaning up...
```
### クレデンシャルが間違っている

401が返ってきて、AuthFailureですね

```:terminal
my-mba-2% AWS_PROFILE=foo ./ec2.py --list           
Looks like AWS is down again:
EC2ResponseError: 401 Unauthorized
<?xml version="1.0" encoding="UTF-8"?>
<Response><Errors><Error><Code>AuthFailure</Code><Message>AWS was not able to validate the provided access credentials</Message></Error></Errors><RequestID>a8b9203c-d4c2-4df3-af78-3e46d61d7a1b</RequestID></Response>
```

### AWSへのアクセス権限がない（もしくは足りない）

権限不足だと403が返ってきてきます

```:terminal
my-mba-2% AWS_PROFILE=test ./ec2.py --list 
Looks like AWS is down again:
EC2ResponseError: 403 Forbidden
<?xml version="1.0" encoding="UTF-8"?>
<Response><Errors><Error><Code>UnauthorizedOperation</Code><Message>You are not authorized to perform this operation.</Message></Error></Errors><RequestID>baaf19be-c63c-4426-aeba-2180b38f35cd</RequestID></Response>
```

EC2のDescribe系の権限を付けてあげましょう

```json:policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:Describe*",
      "Resource": "*"
    }
  ]
}
```

### レスポンスのhostvarsが空で返ってきた

こんなかんじです

```bash:terminal
my-mba-2% AWS_PROFILE=default ./ec2.py --list
{
  "_meta": {
    "hostvars": {}
  }
}
```

このスクリプトは初期設定では、起動しているインスタンスのみ取得します。
なので、インスタンスが一台も起動していないと、上記のようなレスポンスになります。

```ini:ec2.iniから抜粋
# By default, only EC2 instances in the 'running' state are returned. Set
# 'all_instances' to True to return all instances regardless of state.
all_instances = False
```

もしくは、Filterしてた。

ec2.iniファイルの設定で出力するインスタンスをフィルターすることができます。
うっかりここが有効になっていると意図した結果が得られないケースがあります。

挙動が不自然だったら、まずec2.iniの設定を確認してみるといいかもしれませんね。

```ini:ec2.iniから抜粋
# You can use wildcards in filter values also. Below will list instances which  
# tag Name value matches webservers1*                                           
# (ex. webservers15, webservers1a, webservers123 etc)-                          
instance_filters = tag:Name=webservers1*                                       
```