経緯
弊社ではクラウド型eラーニングシステムeden LMSというサービスを運用しているのですが、先日お客さんとこういうやりとりがありました。
「eden LMSってIPアドレス制限の設定はできますか?」
「はい、できますよ。」
「192.168.1.0/24みたいなパターンもありますけど対応できますか?」
「もちろんです。ちなみに許可するパターンの数はいくつくらいですか?」
「2000個以上ありますね」
「2000個以上」
「あと、リストの内容はたびたび変わります」
「お、おう…」
どうやって実装するか?
eden LMSはPythonで書かれており、Python製のnetaddrというライブラリを使えば「192.168.1.100/32は192.168.1.0/24に含まれるか?」みたいな判定自体は簡単に実装できます。ただし、サブネットがバラバラだと許可するIPアドレスリストに含まれるかどうかの判定をリストの要素数だけ行う必要が出てきてしまいます。これは馬鹿正直に実装すると結構なCPU負荷になりそうです。マルチテナント型のクラウドサービスなので、そういう処理は避けなければいけません。
「192.168.1.0/24」みたいなリストをすべて「192.168.1.1、192.168.1.2、192.168.1.3...」のような文字列のリストとして展開し、それに対してアクセス元のIPアドレスの文字列はリストの含まれるか、という実装(ipaddr in allowed_listみたいな判定)でも十分速度は出そうです…が、せっかくなのでAWS Lambdaを使ってマイクロサービス化してしまうことにしました。
AWS Lambdaを使うと、以下のような理由で実装を単純にしておくことが出来ます。
- CPU負荷はほとんど気にしなくてよくなるため、「192.168.1.0/24」みたいなリストもnetaddrを使って判定すればいい
- AWS Lambda側のファイルにIPアドレスのリストを含めておき、リストが更新されたときはサービス本体は触らずにLambda側だけを更新すればよい
実際のソースコード
まず、AWS Lambda側ですが、netaddrライブラリを使ったIPアドレスの判定をAWS Lambdaの形式に合わせてあげるだけです。
from netaddr.ip import IPNetwork,IPAddress
def lambda_handler(event, context):
ipaddr = event['ipaddr']
for network in allowed_networks:
if IPAddress(ipaddr) in network:
return True
return False
allowed_networks = [
IPNetwork("192.168.2.1/24")
,IPNetwork("192.168.3.1/24")
,IPNetwork("192.168.4.1/32")
]
このlambda_function.pyとnetaddrライブラリのフォルダをzipで固めてAWS Lambda側にアップロードした後、API endpointを追加します。これで準備は完了です。
APIを呼び出す側は次のようになります。(ひとまずはSecurityはOpenにしていますが、実際はIAMなどを使うことになるはずです)
# -*- coding: utf-8 -*-
import urllib2
endpoint = "https://xxx.amazonaws.com/prod/apiname" #実際のエンドポイントが入ります
req = urllib2.Request(endpoint)
req.add_header('Content-Type','application/json')
response = urllib2.urlopen(req,'{"ipaddr":"192.168.4.1"}')
print response.read()
あとは判定済の結果をmemcachedなどのKVSにでもキャッシュしておけば、AWS Lambdaの呼び出しは最小限で済みそうです。
まとめ
「ピークのときとそうでないときの処理量の差が大きい」かつ「参照透過である」処理については、AWS Lambdaでどしどしマイクロサービス化してしまうとラクなんじゃないでしょうか。