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

  • 0
    いいね
  • 0
    コメント

    はじめに

    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許可

    ディレクトリ構成

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

    inventory

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

    hosts/aws
    [aws]
    localhost
    

    vars

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

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

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

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

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

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

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

    roles/vpc/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
    
    - 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

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

    実行

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

    まとめ

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

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

    参考