【Boto3】セキュリティグループのルール一覧を作成する

記事タイトルとURLをコピーする

【Boto3】セキュリティグループのルール一覧を作成する

エンジョイ AWS!
サーバーワークス エンジニアの伊藤Kです。

今日は、Boto3を使って、「セキュリティグループのルール一覧」を作成します。
Pythonスクリプトを実行すると、セキュリティグループごとにテキストファイルが出力され、中にルール一覧が記載されているイメージです。

ところで皆さん、Boto3の「Boto」ってそもそも何か、気になりませんか?
私は秋刀魚の価格の次ぐらいに気になったので調べたところ、アマゾン川に住むイルカのことらしいです。
このイルカの動きに着目すると、アマゾン一帯の生態系をナビゲートできるとか。
https://github.com/boto/boto3/issues/1023

転じて、AWSの情報をナビゲートするSDKになるように、つけられた名前なのでしょう。
ますますBoto3に愛着がわいてきました!

さて、Boto3をまずは使って、感覚をつかんでみたい方は、この記事が一推しです。
【Python3入門】boto3を使ってPython3とAWSに触れてみよう!【60分】
記事を書いたのは、サーバーワークス期待のトップエンジニア、柿﨑氏。
彼との思い出は、一緒のプロジェクトに参画したときのこと。
プロジェクトの忙しさのピークに私が風邪で発熱し、離脱を余儀なくされた際に、
涼しい顔をして穴埋めをしてくれたことです。
それ以来、彼には頭が上がりません。
一緒に人狼をしていても、涼しい顔をして私が疑われるように仕向けてくる、実力者です。


サンプルスクリプト

前置きが長くなりました。
早速サンプルスクリプトを掲載します。

※ 注意点

  • 本スクリプトを利用した結果に関して、本記事の筆者並びに著作権者は保証いたしません。 また、一切の責任も負いません。 ご自身の責任でご利用ください。
  • 「pythonで書くならもっといい書き方があるだろ」「変数名にセンスがない」などについては目をつぶってください。サンプルという位置づけで掲載いたしますので、使われる方がご自分のセンスでカスタマイズしていただけますと幸いです。

動作確認環境

  • 実行場所:自端末
  • OS:Windows 10
  • Pythonバージョン:3.7.1

sg1ran-csv.py

#!/usr/bin/env python
 
###
# SG一覧表示
# 引数:なし
# アカウントの全SGの一覧を取得する
###
 
import boto3
import sys
from boto3.session import Session
 
# AWS CLI用 プロファイルを使用した認証
profile = 'xxxxx' # ここの xxxxx を設定済みのプロファイル名に変更します
session = Session(profile_name=profile)
 
def main():
 
    token = ''
 
    # ec2-clientオブジェクト作成
    ec2cl = session.client('ec2')
 
    while token is not None: # 一度に1000個までの制限があるのだ
        response = ec2cl.describe_security_groups(
            MaxResults=1000,
            NextToken=token
        )
 
        num = 0
 
        for sgalist in response['SecurityGroups']:
 
            strFname = sgalist['GroupName'] + '.csv' # 書き込みファイル名設定
 
            # 書き込みファイルをオープン
            fw = open(strFname,'w')
 
            # 見出し情報取得と出力
            fw.write('{}\n'.format('【SG名】,【GroupID】,【VpcId】,【説明】') )
            fw.write('{}\n'.format(sgalist['GroupName'] + ',' + sgalist['GroupId'] + ',' + sgalist['VpcId'] + ',' + sgalist['Description']) )
 
            # インバウンド内容出力
            fw.write('{}\n'.format('【インバウンド】') )
            fw.write('{}\n'.format('【プロトコル】,【FromPort】,【ToPort】,【ソース(IP CIDR or SGID)】,【説明】') )
            for ingresslist in sgalist['IpPermissions']:
 
                # 同じポートは全行に表示させるため入れ子のループ
                if ingresslist['IpRanges'] != []: # IpRangesが空のケースもある(下の3つのいずれかに当てはまる)
                    for iprangelist in ingresslist['IpRanges']:
 
                        if 'Description' in iprangelist: # Descriptionが空のケース対応
                            strDescription = iprangelist['Description']
                        else:
                            strDescription = ''
 
                        if ingresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + iprangelist['CidrIp'] + ',' + strDescription ))
 
                        elif 'FromPort' in ingresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(ingresslist['IpProtocol'] + ',' + str(ingresslist['FromPort']) + \
                                ',' + str(ingresslist['ToPort']) + \
                                ',' + iprangelist['CidrIp'] + ',' + strDescription ))
 
                if ingresslist['Ipv6Ranges'] != []: # Ipv6Rangesのケース
                    for ipv6rangelist in ingresslist['Ipv6Ranges']:
 
                        if 'Description' in ipv6rangelist: # Descriptionが空のケース対応
                            strDescription = ipv6rangelist['Description']
                        else:
                            strDescription = ''
 
                        if ingresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + ipv6rangelist['CidrIpv6'] + ',' + strDescription ))
 
                        elif 'FromPort' in ingresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(ingresslist['IpProtocol'] + ',' + str(ingresslist['FromPort']) + \
                                ',' + str(ingresslist['ToPort']) + \
                                ',' + ipv6rangelist['CidrIpv6'] + ',' + strDescription ))
 
                if ingresslist['PrefixListIds'] != []: # PrefixListIdのケース
                    for pflist in ingresslist['PrefixListIds']:
 
                        if 'Description' in pflist: # Descriptionが空のケース対応
                            strDescription = pflist['Description']
                        else:
                            strDescription = ''
 
                        if ingresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + pflist['PrefixListId'] + ',' + strDescription ))
 
                        elif 'FromPort' in ingresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(ingresslist['IpProtocol'] + ',' + str(ingresslist['FromPort']) + \
                                ',' + str(ingresslist['ToPort']) + \
                                ',' + pflist['PrefixListId'] + ',' + strDescription ))
 
                if ingresslist['UserIdGroupPairs'] != []: # ソースがSGIDのケース
                    for scsglist in ingresslist['UserIdGroupPairs']:
 
                        if 'Description' in scsglist : # Descriptionが空のケース対応
                            strDescription = scsglist ['Description']
                        else:
                            strDescription = ''
 
                        if ingresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + scsglist ['GroupId'] + ',' + strDescription ))
 
                        elif 'FromPort' in ingresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(ingresslist['IpProtocol'] + ',' + str(ingresslist['FromPort']) + \
                                ',' + str(ingresslist['ToPort']) + \
                                ',' + scsglist ['GroupId'] + ',' + strDescription ))
 
            fw.write('{}\n'.format('【アウトバウンド】'))
            fw.write('{}\n'.format('【プロトコル】,【FromPort】,【ToPort】,【宛先(IP CIDR or SGID)】,【説明】') )
            for egresslist in sgalist['IpPermissionsEgress']:
 
                # 同じポートは入れ子のループ(インバウンドと同じ)
                if egresslist['IpRanges'] != []: # IpRangesが空のケースもある(下の3つのいずれかに当てはまる)
                    for iprangelist in egresslist['IpRanges']:
 
                        if 'Description' in iprangelist: # Descriptionが空のケース対応
                            strDescription = iprangelist['Description']
                        else:
                            strDescription = ''
 
                        if egresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + iprangelist['CidrIp'] + ',' + strDescription ))
 
                        elif 'FromPort' in egresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(egresslist['IpProtocol'] + ',' + str(egresslist['FromPort']) + \
                                ',' + str(egresslist['ToPort']) + \
                                ',' + iprangelist['CidrIp'] + ',' + strDescription ))
 
                if egresslist['Ipv6Ranges'] != []: # Ipv6Rangesのケース
                    for ipv6rangelist in egresslist['Ipv6Ranges']:
 
                        if 'Description' in ipv6rangelist: # Descriptionが空のケース対応
                            strDescription = ipv6rangelist['Description']
                        else:
                            strDescription = ''
 
                        if egresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + ipv6rangelist['CidrIpv6'] + ',' + strDescription ))
 
                        elif 'FromPort' in egresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(egresslist['IpProtocol'] + ',' + str(egresslist['FromPort']) + \
                                ',' + str(egresslist['ToPort']) + \
                                ',' + ipv6rangelist['CidrIpv6'] + ',' + strDescription ))
 
                if egresslist['PrefixListIds'] != []: # PrefixListIdのケース
                    for pflist in egresslist['PrefixListIds']:
 
                        if 'Description' in pflist: # Descriptionが空のケース対応
                            strDescription = pflist['Description']
                        else:
                            strDescription = ''
 
                        if egresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + pflist['PrefixListId'] + ',' + strDescription ))
 
                        elif 'FromPort' in egresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(egresslist['IpProtocol'] + ',' + str(egresslist['FromPort']) + \
                                ',' + str(egresslist['ToPort']) + \
                                ',' + pflist['PrefixListId'] + ',' + strDescription ))
 
                if egresslist['UserIdGroupPairs'] != []: # ソースがSGIDのケース
                    for scsglist in egresslist['UserIdGroupPairs']:
 
                        if 'Description' in scsglist : # Descriptionが空のケース対応
                            strDescription = scsglist ['Description']
                        else:
                            strDescription = ''
 
                        if egresslist['IpProtocol'] == '-1': # IpProtocolが-1の場合(全通信)
                            fw.write('{}\n'.format('all,all,all' + \
                                ',' + scsglist ['GroupId'] + ',' + strDescription ))
 
                        elif 'FromPort' in egresslist: # Fromportがキーとして存在しないパターンもある
                            fw.write('{}\n'.format(egresslist['IpProtocol'] + ',' + str(egresslist['FromPort']) + \
                                ',' + str(egresslist['ToPort']) + \
                                ',' + scsglist ['GroupId'] + ',' + strDescription ))
 
            # 書き込みファイルをクローズ
            fw.close()
 
        if 'NextToken' in response:
            token = response['NextToken']
        else:
            token = None
 
 
if __name__ == '__main__':
    main()

実行は、プロファイル指定部分(# AWS CLI用 プロファイルを使用した認証 のxxxxx)を書き替えてから、
特に引数は指定せずに、pythonスクリプトを実行するだけです。

python sg1ran-csv.py

アカウント内の全セキュリティグループの基本情報、ルール一覧が、「セキュリティグループ名.csv」としてセキュリティグループ毎に出力されます。
Excelで扱いやすいようにCSV形式で出力していますが、出力の部分を書き替えることで、いろいろな形式で出せます。

「すべての通信」を「all」と表記させている割にはICMPの場合は「-1」と表記されてしまうのが少し残念ですが、
そこの改善はまたいずれ。
見出しの付け方も、用途によっていろいろとカスタマイズの余地がありそうです。

参照したBoto3のドキュメント

EC2.Client のdescribe_security_groups です。
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_security_groups

セキュリティグループは、EC2のカテゴリなんですね。
このあたりのカテゴリの感覚も、だいぶ慣れてきました。
マネジメントコンソールのカテゴリ分けがヒントになります。

それでは、楽しいAWSライフを!

f:id:swx-kenichi-ito:20201109131210p:plain
Boto3Go

伊藤K (記事一覧)

SRE部

おっさんエンジニア。

甘いお菓子が大好きですが、体重にDirect Connectなので控えています。

「エンジョイ AWS」を合言葉に、AWSを楽しむことを心がけています。