AWSのセキュリティグループをAnsibleで管理する


#はじめに
AnsibleにはAWSのリソースを操作できるモジュールが豊富に用意されています。

今回は、複雑になりがちなセキュリティグループをAnsibleで管理してみます。

#やること
- セキュリティグループ作成
- ルール作成（インバウンドのみ）


##ポイント
セキュリティグループは、通信元、宛先をIPアドレスではなくセキュリティグループを指定することもできるため、Ansibleで作成する際に矛盾が生じることもあります。

例えば、
A：Bからの通信を許可
B：Aからの通信を許可
のようなケースの場合、Aの作成時にはBが存在しないとエラーになりそうですが、実際はルールが空の状態でBも作成されます。

Ansibleで構成する場合、YAMLであるべき構成を定義していくことになります。
`with_dict`でループさせて作成する場合、ループの順序が考慮されないため、Aの作成時にBが存在しない等でタスクがfailしてしまうことがありますが、上記の通り、空のBが作成されていますので、もう一度タスクを実行すると成功します。

このあたりの挙動に気をつけて実装していきます。

#前提
AWS関連のモジュール実行にはbotoが必要です。
credential情報は環境変数か`aws configure`でセットしてある必要があります。

#sample

以下のようなセキュリティグループを作成します。

- common
	- xxx.xxx.xxx.xxxからSSH許可
	- commonからのICMPを全て許可
- web_server
	- AnyからHTTP許可
- db_server
	- web_serverからMySQL許可


##ディレクトリ構成

```bash:ディレクトリ構成
site.yml
roles/
|--security_group/
|  |--tasks/
|  |  |--main.yml
hosts/aws    #inventory
host_vars/
|--localhost.yml
```

##inventory

AWSリソース関連モジュールはすべてlocalhostで実行するので、下記のようなインベントリファイルを用意します。

```bash:hosts/aws
[aws]
localhost
```

##vars

こんな感じに変数を定義します。今回はhost_varsで定義しました。

```yaml:host_vars/localhost.yml
---
my_vars:
  aws:
    common:
      region: ap-northeast-1
    vpc:
      name: AnsibleVPC    # ターゲットのVPC名
    security_group:
      common:
        group_name: common
        description: common rules
        rules:
          - proto: tcp
            from_port: 22
            to_port: 22
            cidr_ip: xxx.xxx.xxx.xxx/32
          - proto: icmp
            from_port: -1
            to_port: -1
            group_name: common
      web_server:
        group_name: web_server
        description: web_server rules
        rules:
          - proto: tcp
            from_port: 80
            to_port: 80
            cidr_ip: 0.0.0.0/0
      db_server:
        group_name: db_server
        description: db_server rules
        rules:
          - proto: tcp
            from_port: 3306
            to_port: 3306
            group_name: web_server

```

##Role

まずVPCを特定するためにidが必要ですが、[こちら](http://qiita.com/tomohiko_isobe/items/c245bedfa6966305eb10)と同様、VPC名でidを取得します。

今回はリストではなくディクショナリとしてセキュリティグループを定義しましたので、`with_dict`でループさせます。

ルールの定義はリストで複数指定できますので、そのようにvarsに定義しました。
このままカンマで繋げてtaskで使用します。

ただし、１つしか定義されていない場合、不要なカンマがついてしまうので、要素数で処理を分岐させています。

アウトバウンドは、多くの場合デフォルト（全許可）で使用すると思うので、vars未定義の場合デフォルト値が採用されるようにしています。

セキュリティグループの構成上、一度目のtaskがfailした場合、リトライするようにしました。


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

- name: セキュリティグループ作成
  ec2_group:
    name: "{{ item.value.group_name }}"
    description: "{{ item.value.description }}"
    region: "{{ my_vars.aws.common.region }}"
    vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
    rules: >-
      {%- if item.value.rules|length == 1 -%}
        {{ item.value.rules }}
      {%- else -%}
        {{ item.value.rules | join(',') }}
      {%- endif -%}
    rules_egress: >-
      {%- if item.value.rules_egress is undefined -%}
        {%- set egress_item = {"proto":"all", "from_port":"0", "to_port":"65535", "cidr_ip":"0.0.0.0/0"} -%}
        {%- set egress = [egress_item] -%}
        {{ egress }}
      {%- elif item.value.rules_egress|length == 1 -%}
        {{ item.value.rules_egress }}
      {%- else -%}
        {{ item.value.rules_egress | join(',') }}
      {%- endif -%}
  with_dict: "{{ my_vars.aws.security_group }}"
  when: my_vars.aws.security_group is defined
  ignore_errors: yes
  register: sg

- debug: var=sg

- name: セキュリティグループ作成リトライ
  ec2_group:
    name: "{{ item.value.group_name }}"
    description: "{{ item.value.description }}"
    region: "{{ my_vars.aws.common.region }}"
    vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
    rules: >-
      {%- if item.value.rules|length == 1 -%}
        {{ item.value.rules }}
      {%- else -%}
        {{ item.value.rules | join(',') }}
      {%- endif -%}
    rules_egress: >-
      {%- if item.value.rules_egress is undefined -%}
        {%- set egress_item = {"proto":"all", "from_port":"0", "to_port":"65535", "cidr_ip":"0.0.0.0/0"} -%}
        {%- set egress = [egress_item] -%}
        {{ egress }}
      {%- elif item.value.rules_egress|length == 1 -%}
        {{ item.value.rules_egress }}
      {%- else -%}
        {{ item.value.rules_egress | join(',') }}
      {%- endif -%}
  with_dict: "{{ my_vars.aws.security_group }}"
  when: my_vars.aws.security_group is defined and sg | failed
  register: sg_retry

- debug: var=sg_retry
```

##site.yml
```YAML:site.yml
---
- name: security_group
  hosts: localhost
  connection: local
  roles:
    - role: security_group
```

##実行
```bash:Command
$ ansible-playbook -i hosts/aws -l localhost site.yml
```

#まとめ
同じタスクを２つ定義するのがカッコ悪いですが、なんとかセキュリティグループを管理できるようになりました。

セキュリティグループはマネコンからだとどういう構成になっているか把握しづらいですが、AnsibleのYAMLで管理すると見通しがよくなるのではないでしょうか。

#参考
- [VPCをAnsibleで作成する](http://qiita.com/tomohiko_isobe/items/c245bedfa6966305eb10)
- [IAMユーザをAnsibleで管理する](http://qiita.com/tomohiko_isobe/items/5ae6f15f52a2d20e2635)