AWSの各サービスで利用されているIPアドレスの更新差分をLambdaとSNSで通知してみる

ご機嫌いかがでしょうか、豊崎です。

前回に引き続きAWSの各サービスで利用されているIPアドレスについての記事になります。

前回のブログはこちら

AWSの各サービスで使用されているIPアドレスを確認する方法 Windows/Linux/Python3

AWSではAWSのサービスで利用しているIPアドレスをwebで公開しています。 https://ip-ranges.amazonaws.com/ip-ranges.json

AWSの各サービスのIPアドレスは不定期で追加されることがあり、上記URLの内容は更新されていきます。 今回は内容の差分を取得したかったので、S3+Lambda+SNSで更新差分を通知する仕組みを作ってみました。

こんな感じです。

ip-ranges.jsonファイルの更新があったタイミングでLambdaが通知を受け取り、内容の差分を確認してユーザにメール通知する想定です。 AmazonIpSpaceChanged トピックが北バージニア(us-east-1)から通知されるので、今回の作業は全て北バージニアで行なっています。

それでは早速作っていきましょう。

S3

まずはip-ranges.jsonを保存する用のS3バケットを作成します。バケット名は「demo-ipranges」としました。

特別な設定はしていませんが、テスト用にいくつかのファイルを配置しておきました。

SNS

トピックを作成し、差分内容を送信したいメールアドレスでサブスクライブしておきましょう。ここでは「demo-iprange」というトピックを作成し自分のメールアドレスを通知先としました。

Lambda

LambdaにはIAMRoleを割り当てています。S3、SNSの操作権限などが必要なためです。今回のLambda用のIAMRoleにアタッチしたポリシーは以下です。

  • AWSLambdaFullAccess
  • AmazonS3FullAccess
  • AmazonSNSFullAccess

検証のため、大きめに権限を割り当てています。

今回のLambda関数ではjsonファイルの内容を比較して、増えたIPレンジと減ったIPレンジを出力するようにしています。減ったIPレンジは不要かもしれないと思ったのですが、使用しているIPレンジの変更やIPレンジ追加に伴うネットワークアドレスの統合表記(/24から/23など)などの可能性を考えて一応差分を確認しています。

設定したLambdaファンクションは以下です。

言語:python3.6

import urllib.request
import json
import boto3
import os
import sys

s3 = boto3.resource('s3')
s3client = boto3.client('s3')
sns = boto3.client('sns', region_name='us-east-1')

def lambda_handler(event, context):
  ## Webで現在公開されているAWSサービスのIPレンジのJSONを取得 iprangesに格納
  url = 'https://ip-ranges.amazonaws.com/ip-ranges.json'
  req = urllib.request.Request(url)
  with urllib.request.urlopen(req) as res:
    ipranges = json.load(res)


  ##ip-ranges.jsonをS3に保存(日付をつける)
  bucket_name = os.environ['BUCKET_NAME']
  json_key = 'ipranges-' + ipranges['createDate'] + '.json'

  obj = s3.Object(bucket_name,json_key)

  obj.put(Body = json.dumps(ipranges))


  ## 比較対象がない場合は処理を終了
  s3objlist = []
  s3obj=s3client.list_objects(Bucket=bucket_name)
  if len(s3obj['Contents']) == 1:
    return 'nothing old ipranges'
  
  ## 一つ前のip-ranges.jsonを読み込む
  for i in s3obj['Contents']:
    s3objlist.append(i['Key'])

  s3objlist.sort(reverse=True)
  res = s3client.get_object(Bucket=bucket_name, Key=s3objlist[1])
  oldipranges = json.loads(res['Body'].read())

  ## 差分(増えた内容)を比較

  diff_v4ipinc = []
  diff_v6ipinc = []

  for new in ipranges['prefixes']:
    eq_flag = 0
    for old in oldipranges['prefixes']:
      if new['region'] == old['region']:
        if new['service'] == old['service']:
          if new['ip_prefix'] == old['ip_prefix']:
            eq_flag = 1
    if eq_flag == 0:
      diff_v4ipinc.append(new)

  for new in ipranges['ipv6_prefixes']:
    eq_flag = 0
    for old in oldipranges['ipv6_prefixes']:
      if new['region'] == old['region']:
        if new['service'] == old['service']:
          if new['ipv6_prefix'] == old['ipv6_prefix']:
            eq_flag = 1
    if eq_flag == 0:
      diff_v6ipinc.append(new)

  ## 差分(減った内容)を比較

  diff_v4ipdec = []
  diff_v6ipdec = []

  for old in oldipranges['prefixes']:
    eq_flag = 0
    for new in ipranges['prefixes']:
      if new['region'] == old['region']:
        if new['service'] == old['service']:
          if new['ip_prefix'] == old['ip_prefix']:
            eq_flag = 1
    if eq_flag == 0:
      diff_v4ipdec.append(old)

  for old in oldipranges['ipv6_prefixes']:
    eq_flag = 0
    for new in ipranges['ipv6_prefixes']:
      if new['region'] == old['region']:
        if new['service'] == old['service']:
          if new['ipv6_prefix'] == old['ipv6_prefix']:
            eq_flag = 1
    if eq_flag == 0:
      diff_v6ipdec.append(old)


  ### 差分を整形しSNS Publish

  snsmsg = ipranges['createDate'] +'.json と '+ s3objlist[1] +'の比較\n\n'

  snsmsg = snsmsg + '---増えた内容(' + ipranges['createDate'] + '.jsonにだけ記載されている)は以下---\n\n'

  for msg in diff_v4ipinc:
    snsmsg = snsmsg + 'region : ' + msg['region'] + ' / service : ' + msg['service'] + ' / ip_prefix : ' + msg['ip_prefix'] + '\n'

  for msg in diff_v6ipinc:
    snsmsg = snsmsg + 'region : ' + msg['region'] + ' / service : ' + msg['service'] + ' / ipv6_prefix : ' + msg['ipv6_prefix'] + '\n'

  snsmsg = snsmsg + '\n---減った内容(' + s3objlist[1] + 'にだけ記載されている)は以下---\n\n'

  for msg in diff_v4ipdec:
    snsmsg = snsmsg + 'region : ' + msg['region'] + ' / service : ' + msg['service'] + ' / ip_prefix : ' + msg['ip_prefix'] + '\n'

  for msg in diff_v6ipdec:
    snsmsg = snsmsg + 'region : ' + msg['region'] + ' / service : ' + msg['service'] + ' / ipv6_prefix : ' + msg['ipv6_prefix'] + '\n'

  sns_responce = sns.publish(
      TopicArn = os.environ['TOPIC_ARN'],
      Message = snsmsg,
      Subject = 'diff-ipranges'
      
  )

  return 'Compleate!'

ちなみにS3バケット名とSNSのTOPIC_ARNは環境変数に登録して、タイムアウトも3分にしています。

事前にS3に配置しておいたjsonファイルの中身を弄って差分(増減)がちゃんと通知されるかを確認してみました。

Lambdaを実行して届いた通知がこちら。ちゃんと届いていますね。

 

<2019/02/04 追記>

実際の更新で正しく通知されていることを確認しました。

 

さいごに

意外とIPの差分を確認したいという要望が出てくることがありましたので、作ってみました。

テストでは成功しましたが、実際の変更での通知がAmazonIpSpaceChanged トピックからまだ来てないので、もうしばらく様子見してみます。 何か問題があれば修正するかもしれません。誰かの参考になれば幸いです。