RDSをAnsibleで管理する


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

今回は、RDSをAnsibleで管理してみます。
RDSインスタンスを作成するためには、

- サブネットグループ
- パラメータグループ

が必要になるので、一気通貫で作成できるようにします。

#やること
- サブネットグループ作成
- パラメータグループ作成
- RDSインスタンス作成


##ポイント
サブネットグループを作成するためにサブネットIDが、RDSインスタンスを作成するためにセキュリティグループIDがそれぞれ必要となりますが、IDをAnsibleのYAMLに書きたくないので、それぞれ名前からIDを取得する実装とします。

また、Ansibleのrdsモジュールにはなぜかストレージタイプを指定するためのオプションがなく、作成されたインスタンスのストレージはマグネティックになってしまいますので、インスタンス作成の部分は`shell`モジュールで`aws cli`を利用して実装しています。

冪等性担保のために、インスタンス作成前に同名のインスタンスの存在確認をして、その結果に基づいてインスタンス作成処理を実行します。

##注意
RDSのストレージタイプには、マグネティック、汎用SSD、プロビジョンドIOPS SSDがありますが、今回のPlaybookはプロビジョンドIOPS SSDには対応していません（プロビジョンドIOPSパラメータを定義していないため）。

#前提
- AWS関連のモジュール実行にはbotoが必要です。
- credential情報は環境変数か`aws configure`でセットしてある必要があります。
- [ec2_group_factsが必要](http://qiita.com/tomohiko_isobe/items/8684b4dc8f99793e5cf8)です。

下記リソースを前提に進めます。

- VPC
	- AnsibleVPC
- サブネット
	- private-a
	- private-c
- セキュリティグループ
	- db_server



#sample

以下のようなRDSインスタンスを作成します。

- サブネットグループ
	- private
		- private-a(AZ-a)
		- private-c(AZ-c)
- パラメータグループ
	- mysql57-sample
- RDSインスタンス
	- sample-db
		- セキュリティグループ
			- db_server
		- サブネットグループ
			- private
		- パラメータグループ
			- mysql57-sample


##ディレクトリ構成

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

##inventory

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

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

##vars

こんな感じに変数を定義します。今回はhost_varsで定義しました。
RDSインスタンスのパスワードをそのまま記載していますが、実際はansible-vaultで暗号化したファイルなどに記載してください。

```yaml:host_vars/localhost.yml
---
my_vars:
  aws:
    common:
      region: ap-northeast-1
    vpc:
      name: AnsibleVPC    # ターゲットのVPC名
    rds:
      subnet_group:
        - name: private
          description: 'private subnet group'
          subnets:
            - private-a #サブネット名
            - private-c #サブネット名
      param_group:
        - name: mysql57-sample
          description: 'MySql5.7 sample'
          engine: mysql5.7
          params:
            character_set_database: utf8
            character_set_server: utf8
      instance:
        - name: sample-db
          db_engine: MySQL
          engine_version: 5.7.16
          multi_zone: no
          zone: a
          size: 5
          instance_type: db.t2.micro
          storage_type: gp2
          parameter_group: mysql57-sample
          subnet_group: private
          security_group:
            - db-server #セキュリティグループ名
          username: ADMIN_USER
          password: ADMIN_PASSWORD
          tags:
            - key: Role
              value: sample-db
            - key: Env
              value: dev
```

##Role

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

後続タスクで必要となる、サブネットIDとセキュリティグループIDを取得し、それぞれ名前からIDを参照するためにディクショナリを生成します。

```yaml:roles/rds/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

- name: subnet id取得
  ec2_vpc_subnet_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
      "tag:Name": "{{ item.1 }}"
  with_subelements:
    - "{{ my_vars.aws.rds.subnet_group }}"
    - subnets
  register: subnet_fact
  when: my_vars.aws.rds.subnet_group is defined

- name: subnet dict作成
  set_fact:
    subnets_dict: >-
      {%- set dict = {} -%}
      {%- set inc = 0 -%}
      {%- set subnet_group_cnt = my_vars.aws.rds.subnet_group|length -%}
      {%- for i in range(subnet_group_cnt) -%}
      {%-   set list = [] -%}
      {%-   for j in range(my_vars.aws.rds.subnet_group[i].subnets|length) -%}
      {%-     set _ = list.append(subnet_fact.results[inc+j].subnets[0].id) -%}
      {%-   endfor -%}
      {%-   set _ = dict.update({my_vars.aws.rds.subnet_group[i].name: list}) -%}
      {%-   set inc = inc + subnet_group_cnt -%}
      {%- endfor -%}
      {{ dict }}
  when: my_vars.aws.rds.subnet_group is defined

- name: securitygroup id取得
  ec2_group_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
      group_name: "{{ item.1 }}"
  with_subelements:
    - "{{ my_vars.aws.rds.instance }}"
    - security_group
  register: sg_fact
  when: my_vars.aws.rds.instance is defined

- name: securitygroup dict作成
  set_fact:
    sg_dict: >-
      {%- set dict = {} -%}
      {%- for i in range(sg_fact.results|length) -%}
      {%-   set _ = dict.update({sg_fact.results[i].security_groups[0].group_name: sg_fact.results[i].security_groups[0].group_id}) -%}
      {%- endfor -%}
      {{ dict }}
  when: my_vars.aws.rds.instance is defined

- name: RDS subnet-group作成
  rds_subnet_group:
    state: present
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    region: "{{ my_vars.aws.common.region }}"
    subnets: >-
      {%- set subnetname = item.name -%}
      {%- set list = subnets_dict[item.name] -%}
      {{ list }}
  with_items: "{{ my_vars.aws.rds.subnet_group }}"
  register: rds_subnet_group

- debug: var=rds_subnet_group

- name: RDS パラメータグループ作成
  rds_param_group:
    state: present
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    region: "{{ my_vars.aws.common.region }}"
    engine: "{{ item.engine }}"
    params: "{{ item.params }}"
  with_items: "{{ my_vars.aws.rds.param_group }}"
  register: rds_param_group
  when: my_vars.aws.rds.param_group is defined

- debug: var=rds_param_group

- name: RDS インスタンス存在確認
  shell: >-
    export AWS_DEFAULT_REGION='{{ my_vars.aws.common.region }}' && \
    aws rds describe-db-instances \
      --db-instance-identifier {{ item.name }} \
      --query 'DBInstances[].DBInstanceIdentifier' \
      --output text
  with_items: "{{ my_vars.aws.rds.instance }}"
  check_mode: no
  failed_when: no
  changed_when: no
  register: rds_instance_chk
  when: my_vars.aws.rds.instance is defined

- name: RDS インスタンス list作成
  set_fact:
    rds_list: >-
      {%- set rdslist = [] -%}
      {%- for i in range(rds_instance_chk.results|length) -%}
      {%-   if rds_instance_chk.results[i].stdout != "" -%}
      {%-     set _ = rdslist.append(rds_instance_chk.results[i].stdout) -%}
      {%-   endif -%}
      {%- endfor -%}
      {{ rdslist }}
  check_mode: no

- name: RDS インスタンス作成
  shell: >-
    export AWS_DEFAULT_REGION='{{ my_vars.aws.common.region }}' && \
    aws rds create-db-instance \
      --db-instance-identifier {{ item.name }} \
      --db-instance-class {{ item.instance_type }} \
      --allocated-storage {{ item.size }} \
      --engine {{ item.db_engine }} \
      --master-username {{ item.username }} \
      --master-user-password {{ item.password }} \
      --vpc-security-group-ids \
      {% set sg_list = [] %}
      {%- for i in range(item.security_group|length) -%}
      {%-   set _ = sg_list.append(sg_dict[item.security_group[i]]) -%}
      {%- endfor -%}
      {{ sg_list | join(' ') }} \
      --availability-zone {{ my_vars.aws.common.region }}{{ item.zone }} \
      --db-subnet-group-name {{ item.subnet_group }} \
      --db-parameter-group-name {{ item.parameter_group }} \
      {% if item.multi_zone == True %}
      --multi-az \
      {% else %}
      --no-multi-az \
      {% endif %}
      --engine-version {{ item.engine_version }} \
      --storage-type {{ item.storage_type }} \
      --copy-tags-to-snapshot \
      --tags \
      {% set tag_list = [] %}
      {%- for i in range(item.tags|length) -%}
      {%-   set _ = tag_list.append("Key="+item.tags[i]['key']+",Value="+item.tags[i]['value']) -%}
      {%- endfor -%}
      {{ tag_list | join(' ') }}
  with_items: "{{ my_vars.aws.rds.instance }}"
  when: >-
    not ansible_check_mode
    and '{{ item.name }}' not in rds_list
```

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

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


#まとめ
RDSは、関連リソースを合わせて作成する必要がありますが、Ansibleで自動化できると楽です。
また、今回のplaybookはインスタンスの変更には対応していません。

参考になれば幸いです。

#参考
- [VPCをAnsibleで作成する](http://qiita.com/tomohiko_isobe/items/c245bedfa6966305eb10)
- [IAMユーザをAnsibleで管理する](http://qiita.com/tomohiko_isobe/items/5ae6f15f52a2d20e2635)
- [AWSのセキュリティグループをAnsibleで管理する](http://qiita.com/tomohiko_isobe/items/7342b8ba874d997c5c6c)
- [EC2をAnsibleで管理する](http://qiita.com/tomohiko_isobe/items/163b23d5a0e566aaa159)
- [ec2_group_factsモジュールをAnsible2.2で使えるようにする](http://qiita.com/tomohiko_isobe/items/8684b4dc8f99793e5cf8)