AnsibleによるVPCの構成管理

ANSIBLE

渡辺です。

AnsibleによるAWSリソースの構成管理シリーズ、 今回はVPC(ネットワーク)周りを構築するAnsible Playbookを紹介します。 Ansible2.0で追加されたモジュールを利用しているため、現行のAnsible2.1系でも利用可能です。

グループ変数のサンプル

今回紹介するPlaybookで定義するグループ変数のサンプルです。

env: Prd
profile: default
region: ap-northeast-1
vpc:
  name: VPC
  cidr_blodk: 10.0.0.0/16
subnet:
  - name: PublicA
    az: ap-northeast-1a
    cidr: 10.0.1.0/24
    route: Public
  - name: PublicC
    az: ap-northeast-1c
    cidr: 10.0.2.0/24
    route: Public
  - name: PrivateA
    az: ap-northeast-1a
    cidr: 10.0.11.0/24
    route: Private
  - name: PrivateC
    az: ap-northeast-1c
    cidr: 10.0.12.0/24
    route: Private

VPCとサブネットを構成するにあたって必要最低限の情報がスッキリとまとまっているかと思います。 この定義、すなわちグループ変数を読み込んで、VPCを作るのが目標です。

VPC Playbook

VPC構築用のPlaybookです。

---
- hosts: localhost
  connection: local
  gather_facts: False
  tasks:
    - name: "{{ env }} VPC"
      ec2_vpc_net:
        name: "{{ env }} {{ vpc.name }}"
        cidr_block: "{{ vpc.cidr_blodk }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _vpc
    - debug: var=_vpc verbosity=1
    - name: Internet gateway
      ec2_vpc_igw:
        vpc_id: "{{ _vpc.vpc.id }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _igw
    - name: "{{ env }}Public subnet"
      ec2_vpc_subnet:
        vpc_id: "{{ _vpc.vpc.id }}"
        az: "{{ item.az }}"
        cidr: "{{ item.cidr }}"
        resource_tags:
          Name: "{{ item.name }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      with_items: "{{ subnet }}"
      register: _subnet
    - debug: var=_subnet verbosity=1
    - name: Public Route
      ec2_vpc_route_table:
        vpc_id: "{{ _vpc.vpc.id }}"
        tags:
          Name: "{{ env }}PublicRoute"
        subnets: "{{ _subnet.results | selectattr('item.route', 'equalto', 'Public') | map(attribute='subnet.id') | list }}"
        routes:
          - dest: 0.0.0.0/0
            gateway_id: "{{ _igw.gateway_id  }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _public_route
    - debug: var=_public_route verbosity=1
    - name: Private Route
      ec2_vpc_route_table:
        vpc_id: "{{ _vpc.vpc.id }}"
        tags:
          Name: "{{ env }}PrivateRoute"
        subnets: "{{ _subnet.results | selectattr('item.route', 'equalto', 'Private') | map(attribute='subnet.id') | list }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _private_route
    - debug: var=_private_route verbosity=1

ec2_vpc_net

ec2_vpc_netモジュールは、VPCを構築するモジュールです。 似た名前のモジュールでec2_vpcモジュールがありますが、こちらはレガシーなモジュールになっているので注意してください。

    - name: "{{ env }} VPC"
      ec2_vpc_net:
        name: "{{ env }} {{ vpc.name }}"
        cidr_block: "{{ vpc.cidr_blodk }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _vpc

ec2_vpc_netでは、VPC名とCIDRブロックを指定し、VPCを定義します。 VPCの情報は変数_vpcに格納し、他のモジュールでVPC IDなどを指定するために利用します。

Ansibleは冪等性を確保しますので、初回実行時にAWS環境にVPCが作成されます。 2回目の実行を行った場合、VPCが既に存在するので作成はされません。 この時、VPC名で識別しているため、VPC名を変更すると別のVPCとして認識しますので注意しましょう。

ec2_vpc_igw

ec2_vpc_igwモジュールは、Internet Gatewayを構築するモジュールです。

    - name: Internet gateway
      ec2_vpc_igw:
        vpc_id: "{{ _vpc.vpc.id }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _igw

ec2_vpc_igwモジュールでは、VPC IDをパラメータとして指定します。 VPC IDは、ec2_vpc_net実行時の結果に含まれるので、registerで保存した変数_vpcから参照します。

なお、実行結果にどのような構造で情報が格納されているかを確認するには、debugモジュールを利用します。

    - debug: var=_vpc verbosity=1

実行結果のサンプルは次のようになります。

TASK [debug] *******************************************************************
ok: [localhost] => {
    "_vpc": {
        "changed": false, 
        "vpc": {
            "cidr_block": "10.0.0.0/16", 
            "classic_link_enabled": null, 
            "dhcp_options_id": "dopt-xxxxxxxxx", 
            "id": "vpc-xxxxxxxxx", 
            "instance_tenancy": "default", 
            "is_default": false, 
            "state": "available", 
            "tags": {
                "Name": "Prd VPC"
            }
        }
    }
}

ec2_vpc_subnet

ec2_vpc_subnetモジュールはサブネットを構築するモジュールです。

    - name: "{{ env }}Public subnet"
      ec2_vpc_subnet:
        vpc_id: "{{ _vpc.vpc.id }}"
        az: "{{ item.az }}"
        cidr: "{{ item.cidr }}"
        resource_tags:
          Name: "{{ item.name }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      with_items: "{{ subnet }}"
      register: _subnet

ec2_vpc_subnetモジュールでは、VPC IDの他にAZやCIDRなどをパラメータとして指定します。

このモジュール利用時のポイントは、with_items構文を用いて、グループ変数で定義されたsubnetの要素をループ実行している点です。 サブネットの定義を1行づつ行うことも可能ですが、Playbookの再利用性を高くするため、リスト型の変数とループをを活用しました。 これならば、サブネットが多いシステムであってもPlaybookの変更は不要で、グループ変数の変更のみで済むことになります。

with_items構文でループを行う時は、itemという変数に各ループでの要素が代入されます。 itemの各属性は.で参照可能なので直感的でしょう。

ec2_vpc_route_table

ec2_vpc_route_tableモジュールは、Routeテーブルを構築するモジュールです。 PublicRoutePrivateRouteの2つを作成し、サブネットをバインドします。

    - name: Public Route
      ec2_vpc_route_table:
        vpc_id: "{{ _vpc.vpc.id }}"
        tags:
          Name: "{{ env }}PublicRoute"
        subnets: "{{ _subnet.results | selectattr('item.route', 'equalto', 'Public') | map(attribute='subnet.id') | list }}"
        routes:
          - dest: 0.0.0.0/0
            gateway_id: "{{ _igw.gateway_id  }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _public_route
    - name: Private Route
      ec2_vpc_route_table:
        vpc_id: "{{ _vpc.vpc.id }}"
        tags:
          Name: "{{ env }}PrivateRoute"
        subnets: "{{ _subnet.results | selectattr('item.route', 'equalto', 'Private') | map(attribute='subnet.id') | list }}"
        profile: "{{ profile }}"
        region: "{{ region }}"
      register: _private_route

vpc_idgateway_idの参照はもう問題無く理解できると思います。 ここでポイントとなるのは、subnetsです。

subnetsには対象となるサブネットのsubnet idをリストとして指定しなければなりません。 しかし、サブネットはループで作成しており、変数_subnetには複数のサブネットの作成結果が格納されます。 デバッグ出力するとこんなイメージです(長いので一部省略)。

TASK [debug] *******************************************************************
ok: [localhost] => {
    "_subnet": {
        "changed": false, 
        "msg": "All items completed", 
        "results": [
            {
                "item": {
                    "az": "us-east-2a", 
                    "cidr": "10.0.1.0/24", 
                    "name": "FrontA", 
                    "route": "Public"
                }, 
                "subnet": {
                    "id": "subnet-a9c136c0"
                }
            }, 
            {
                "item": {
                    "az": "us-east-2c", 
                    "cidr": "10.0.2.0/24", 
                    "name": "FrontC", 
                    "route": "Public"
                }, 
                "subnet": {
                    "id": "subnet-815970cb"
                }
            }, 
            {
                "item": {
                    "az": "us-east-2a", 
                    "cidr": "10.0.11.0/24", 
                    "name": "ServiceA", 
                    "route": "Public"
                }, 
                "subnet": {
                    "id": "subnet-acc136c5"
                }
            }, 
            {
                "item": {
                    "az": "us-east-2c", 
                    "cidr": "10.0.12.0/24", 
                    "name": "ServiceC", 
                    "route": "Public"
                }, 
                "subnet": {
                    "id": "subnet-9a5970d0"
                }
            }, 
            {
                "item": {
                    "az": "us-east-2a", 
                    "cidr": "10.0.21.0/24", 
                    "name": "DatabaseA", 
                    "route": "Private"
                }, 
                "subnet": {
                    "id": "subnet-a6c136cf" 
                }
            }, 
            {
                "item": {
                    "az": "us-east-2c", 
                    "cidr": "10.0.22.0/24", 
                    "name": "DatabaseC", 
                    "route": "Private"
                }, 
                "subnet": {
                    "id": "subnet-935970d9"
                }
            }
        ]
    }
}

すなわち、results属性にリストとして各ループの実行結果が格納されています。 また、item属性では各ループで利用したitem属性を参照できます。 作成されたサブネットのidsubnet.idで参照できるようです。

プログラマであれば、MapとFilterが使えれば、サブネットIDリストを作れると考えるでしょう。 AnsibleではJinja2の構文が利用できるため、次のようにしてサブネットIDリストに変換します。

_subnet.results | selectattr('item.route', 'equalto', 'Public') | map(attribute='subnet.id') | list
_subnet.results | selectattr('item.route', 'equalto', 'Private') | map(attribute='subnet.id') | list

_subnet.results(リスト)をselectattritem.routeの値でフィルタリングし、subnet.idにマッピングした後、リスト化する、といった流れです。 結果はそれぞれ次のようになります。

["subnet-a9c136c0", "subnet-815970cb", "subnet-acc136c5", "subnet-9a5970d0"]
["subnet-a6c136cf", "subnet-935970d9"]

まとめ

今回はAnsibleのVPC関連モジュールを利用してネットワークを構成するPlaybookを紹介しました。

あわせて、ループやJinja2の構文を活用したプログラマティックなAnsibleのテクニックを紹介しました。 Ansibleでは、このようなループや強力なフィルタができることが大きなメリットです。 しかし、複雑すぎるPlaybookは可読性を低くする可能性があるので注意しなければなりません。 グループ変数の可読性を高くするか、Playbookの可読性を高くするか、バランス良く選択してください。