New Relic SyntheticsモニターのIP範囲をセキュリティグループに反映させるPythonスクリプト

# 概要
AWS上のサーバーに対してNew Relic SyntheticsによってURL監視をする際に、セキュリティグループで制限をかけている場合に、インバウンドルールにNew Relic SyntheticsモニターのIPを許可ルールに追加したいことがある。その追加・更新のPythonスクリプトを書いた


# セキュリティグループの更新スクリプト
## 内容
New Relicで公開されているIPアドレス一覧を取得し、任意のロケーションのIPアドレスを指定のセキュリティグループに対して、その許可ルールを設定する。

https://docs.newrelic.com/docs/synthetics/synthetic-monitoring/administration/synthetic-public-minion-ips/ 

新規追加と更新に対応しており、将来、上記のIPアドレスリストに変更があった場合に、その変更も反映できる。

## インバウンドルール更新時のルールの識別
セキュリティグループのルールにはIDがなく、ルールを指定して更新することができない。
そこで、各ルールの「Description」を利用し、本スクリプトで更新対象とするルールかどうかの識別を行う。

ルール追加時に `New Relic Synthetics Monitor` を識別文字列としてルールのDescriptionに設定し、ルールの更新時にはこのDescriptionが設定されているルールが対象となって更新される。

## 更新の流れ
AWS APIの関係で、対象となったルールは変更がなくても一度全て削除されて改めて追加されるという流れになっている。これは、セキュリティグループ内の全てが対象ではなく、Descriptionによって更新対象と識別されたもののみ。

## スクリプトの利用

ライブラリインストール (初回のみ)

```bash
pip install boto3
pip install requests
```

コード

```python:update_sg.py
import boto3
import copy
import requests

URL_NR_SYNTHETICS_IP_RANGE = "https://s3.amazonaws.com/nr-synthetics-assets/nat-ip-dnsname/production/ip.json"
SG_DESCRIPTION = "New Relic Synthetics Monitor"

# Set location labels
NR_LOCATIONS = [
    "Tokyo, JP",
    "San Francisco, CA, USA"
]


def get_nr_synthetics_ip_range(locations: list):
    res = requests.get(URL_NR_SYNTHETICS_IP_RANGE)

    ips_per_location = res.json()

    ips = []
    for location in locations:
        ips.extend(ips_per_location[location])

    ip_ranges = ["{}/32".format(ip) for ip in ips]
    return ip_ranges


def update_sg(security_group_id: str,
              port: int,
              protocol: str,
              cider_ips: list,
              desc: str,
              aws_profile: str = None,
              aws_region: str = "ap-northeast-1",
              ):
    """
    Update security group
    """
    if aws_profile is not None:
        session = boto3.session.Session(profile_name=aws_profile, region_name=aws_region)
        client = session.client("ec2")
    else:
        client = boto3.client("ec2")

    print("Describe current rules.")
    res = client.describe_security_groups(GroupIds=[security_group_id,])
    security_groups = res["SecurityGroups"]

    if len(security_groups) < 1:
        return
    security_group = security_groups[0]

    print(security_group)

    del_ip_permissions = []

    for ip_perm in security_group["IpPermissions"]:
        if ip_perm.get("FromPort") == port and ip_perm.get("ToPort") == port and ip_perm.get("IpProtocol") == "tcp":
            del_ip_ranges = [ip_range for ip_range in ip_perm["IpRanges"] if ip_range.get("Description") == desc]
            del_ip_perm = copy.deepcopy(ip_perm)
            if len(del_ip_ranges) > 0:
                del_ip_perm["IpRanges"] = del_ip_ranges
                del_ip_permissions.append(del_ip_perm)

    if len(del_ip_permissions) > 0:
        print("Delete current rules.")
        print(del_ip_permissions)
        client.revoke_security_group_ingress(
            GroupId=security_group_id,
            IpPermissions=del_ip_permissions,
        )
    else:
        print("No deletion targets.")

    print("add rules")
    added_ip_ranges = []
    for cidr_ip in cider_ips:
        ip_range = {
            "CidrIp": cidr_ip,
            "Description": desc
        }
        added_ip_ranges.append(ip_range)

    print(added_ip_ranges)
    client.authorize_security_group_ingress(
        GroupId=security_group_id,
        IpPermissions=[
            {
                'FromPort': port,
                'IpProtocol': protocol,
                'IpRanges': added_ip_ranges,
                'ToPort': port,
            },
        ],
    )


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Update security groups to allow New Relic Synthetics Monitor access.")
    parser.add_argument('sg_id', type=str, help="Security Group ID")
    parser.add_argument('--port', type=int, default=80)
    parser.add_argument('--protocol', type=str, default="tcp")
    parser.add_argument('--description', type=str, default=SG_DESCRIPTION)
    parser.add_argument('--aws-profile', type=str, default=None)
    parser.add_argument('--aws-region', type=str, default=None)

    args = parser.parse_args()

    ips = get_nr_synthetics_ip_range(
        NR_LOCATIONS
    )

    update_sg(args.sg_id,
              args.port,
              args.protocol,
              ips,
              args.description,
              args.aws_profile,
              args.aws_region,
              )

```

実行例

```bash
python update_sg.py {セキュリティグループID} --aws-profile {AWSプロファイル} --aws-region {AWSリージョン}
```

実行すると、このように設定される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/77ea4389-28a9-f044-3b1f-c9ed326755cf.png)


### リポジトリ
https://github.com/mmclsntr/update-sg-rules-to-allow-newrelic-synthetics

## 補足
- ポートおよびプロトコル単位での設定となる (デフォルトはTCP:80)。指定する場合は、実行時に `--port`や `--protocol` フラグで指定可能
- ルールのDescriptionを変更する場合は`--description` フラグで指定可能
- Syntheticsのロケーションを変更する場合は、コード内の `NR_LOCATIONS` を編集