はじめに

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が必要です。

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

  • 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

ディレクトリ構成

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

inventory

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

hosts/aws
[aws]
localhost

vars

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

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が必要ですが、こちらと同様、VPC名でidを取得します。

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

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

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

実行

Command
$ ansible-playbook -i hosts/aws -l localhost site.yml

まとめ

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

参考になれば幸いです。

参考