はじめに
この記事はネタ記事です。
本記事で紹介するような、特定の地域からのアクセスブロック等の行為を推奨するものでは一切ございません。
ただし、香川県のパブリックコメントへの回答によると
Q:事業者の対策・協力義務について、具体的にはどのような対策を想定していますか。香川県からのアクセス遮断なども含まれますか。
A:事業者に自主的な取り組みを呼びかけるものと考えています。
【全22問】「1日60分までの根拠は」「なぜ議事録がないのか」―― 香川県「ネット・ゲーム依存症対策条例(仮称)」、議会事務局との一問一答
との回答が得られており、万が一この条例が可決されれば、事業者による自主的な取り組みの一環として、香川県からのアクセスブロックを行う可能性は否定できません。
条例が可決された場合には、香川県からのアクセスをブロックすることが技術的に可能であるかを検証するという目的のもと、本記事を執筆いたしました。
概要
それではどうやって実現しましょう。
では、JavaScriptのGeolocation APIを使って香川からのアクセスをブロックする手法が紹介されていました。
本記事ではもっとシンプルに、AWS Lambda@EdgeからIP infoDBにIPアドレスを問い合わせて、RegionCodeが香川県のものをブロックするという手法を紹介します。AWSリソースの操作にはterraform, aws cliを用いて、Lamdaのデプロイにはlambrollを用いています。
- lambroll: https://github.com/fujiwara/lambroll
- terraform: https://www.terraform.io/
本当はAWS WAFとかでもっと簡単にやりたかったんですが、さすがにWAFでのGeoCodeは国単位だったので不可能でした。残念ですね。
AWS Lambda@Edgeとは
簡単に言えばCloudFrontのエッジサーバ上でLambda Functionを実行することができる機能です。
フックとなるトリガーには下図のイベントが用意されています。
今回は、"Viewer request"をトリガーにすればLambda@Edgeでアクセスブロックを実現できそうですね。
Lambdaのコードを書く
Lambda@EdgeはNodejsとPython3.7しか対応していないので、どちらか好みの方で書きます。Python3を用いてコードを書きました。
# !/usr/bin/env python3
import urllib.request
import json
import re
import os
def lambda_handler(event, context):
key = "XXXXXXXXXXXXXXXXXXXXXXX"
block_region = "Tokyo"
request = event['Records'][0]['cf']['request']
client_ip = request['clientIp']
print("request:\n%s" % request)
url = "http://api.ipinfodb.com/v3/ip-city/"
try:
req = urllib.request.urlopen("%s/?key=%s&ip=%s&format=json" % (url, key, client_ip))
except urllib.error.URLError as e:
print(e.reason)
except urllib.error.HTTPError as e:
print(e.code)
res = req.read().decode('utf-8')
ip_info = json.loads(res)
print("ip_info:\n%s" % ip_info)
if ip_info["regionName"] != block_region:
return request
print("BLOCK REQUEST FROM %s" % block_region)
response = {
'status': '451',
'statusDescription': 'Unavailable For Legal Reasons',
}
return response
挙動は次の通りです。
- requestから
clientIP
(アクセス元のIP)を取り出します - ipinfodb.comのAPIを用いてIPアドレスの
regionName
を取り出します -
block_region
と一致した場合ステータスコード451を返します -
block_region
と一致しなかった場合には通常のレスポンスを返します
いくつか注意点があります
-
https://ipinfodb.com/ に新規登録してAPIキーを発行する必要があります。
-
block_regionが"Tokyo"なんだけど?
- 動作確認のためです。あとで"Kagawa"に変更してください。
-
lambda@Edgeは環境変数に対応していません。APIキーとかもベタ書きにならざるを得ません。物騒ですね。
-
ステータスコード 451 ってなに?
- 筆者も調べて初めて知ったんですが、Unavailable For Legal Reasons つまり **「法的理由により取得不能」**とのこと。今回の要件にぴったり。
- ちなみに有名な小説『華氏451度』にちなんでつけられたそうです。
IAM roleを作成する
lambda実行用のIAM Roleを作成します。terraform で書くとこのようになります。
resource "aws_iam_role" "lambda_edge" {
name = "lambda"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}
data "aws_iam_policy_document" "lambda_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
}
}
}
resource "aws_iam_role_policy_attachment" "lambda_basic_role" {
role = aws_iam_role.lambda_edge.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
- IAMの信頼関係で
"edgelambda.amazonaws.com"
を追加します。 - アクセス権限にCloudWatch Logsにログを流すために
AWSLambdaBasicExecutionRole
のポリシーを付与します。
lambdaをデプロイする
準備ができたら、Lambda@Edgeで使うために**us-east-1(バージニア北部)**リージョンにデプロイします。
terraformでデプロイしてもいいのですが、ここではlambrollというlambdaのデプロイツールを用いました。
- lambroll: https://github.com/fujiwara/lambroll
デプロイ用の設定は以下のとおりです。
$ lambroll init
$ cat function.json
{
"FunctionName": "block",
"Handler": "block.lambda_handler",
"MemorySize": 128,
# さきほど作成したIAM Role
"Role": "arn:aws:iam::XXXXXXXXXXXX:role/lambda",
"Runtime": "python3.7",
"Timeout": 3
}
デプロイを実行します
$ lambroll deploy --region us-east-1
2020/02/07 01:41:02 [info] lambroll v0.3.3
2020/02/07 01:41:02 [info] starting deploy function block
2020/02/07 01:41:03 [info] creating zip archive from .
2020/02/07 01:41:03 [info] zip archive wrote 973 bytes
2020/02/07 01:41:03 [info] creating function
2020/02/07 01:41:03 [info] deployed function version 1
2020/02/07 01:41:03 [info] creating alias set current to version 1
2020/02/07 01:41:04 [info] alias created
2020/02/07 01:41:04 [info] completed
これでlambdaの準備は完了です。
S3を用意する
このあたりはちゃっとやっちゃいましょう。S3を作成します。名前はなんか適当に付けます。
resource "aws_s3_bucket" "web-hosting" {
bucket = "sakutomo-webhosting-test"
acl = "private"
}
できたバケットには適当な画像、香川なんでうどんでもおいておきましょう。
$ aws s3 ./bukkake_udon.png s3://sakutomo-webhosting-test/
つづいて、CloudFrontもつくります。
resource "aws_cloudfront_origin_access_identity" "web-hosting" {
comment = "sakutomo-webhosting-origin-access-identity"
}
resource "aws_cloudfront_distribution" "web-hosting" {
enabled = true
price_class = "PriceClass_All"
comment = "Web Hosting Sakutomo Test"
origin {
domain_name = aws_s3_bucket.web-hosting.bucket_domain_name
origin_id = "S3-sakutomo-webhosting-test.s3.amazonaws.com"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.web-hosting.cloudfront_access_identity_path
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-sakutomo-webhosting-test.s3.amazonaws.com"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
minimum_protocol_version = "TLSv1.1_2016"
ssl_support_method = "sni-only"
}
wait_for_deployment = false
}
オリジンには先程のS3バケットを指定しています。
最後に、CloudFrontからS3においた画像が見られるように、バケットポリシーを作成します。
data "aws_iam_policy_document" "web-hosting" {
statement {
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
aws_s3_bucket.web-hosting.arn,
format("%s/*", aws_s3_bucket.web-hosting.arn),
]
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.web-hosting.iam_arn]
}
}
}
resource "aws_s3_bucket_policy" "web-hosting" {
bucket = aws_s3_bucket.web-hosting.id
policy = data.aws_iam_policy_document.web-hosting.json
}
ここまでできたらCloudFrontのアドレスから先程のうどんが見えるかどうかを確認しましょう。
ちゃんとうどんが見えますね。
CLIでもリターンコードを確認します。
$ curl https://XXXXXXXXXXXXXx.cloudfront.net/bukkake_udon.png -I
HTTP/2 200
content-type: image/png
....
server: AmazonS3
x-cache: Hit from cloudfront
...
Lambd@Edgeを適用する
このうどんを香川県から見えなくします。
先程のterraformに以下のオプションを追加します
resource "aws_cloudfront_distribution" "web-hosting" {
...
default_cache_behavior {
+ lambda_function_association {
+ event_type = "viewer-request"
+ lambda_arn = "arn:aws:lambda:us-east-1:XXXXXXXX:function:block:1"
+ }
これでCloudFrontへのViewerRequest時にLambda@Edgeが実行されるようになりました!!
確認する
本当にうどんが見えなくなったのか確認しましょう。
反映するまでに少々時間がかかります。CloudFrontのDestributionのStatusがグルグルまわる In Progress からDeployedになるまで気長に待ちます。
ぐるぐるが終わったら先程のようにブラウザでアクセスしてみましょう。
$ curl https://XXXXXXXXXXX.cloudfront.net/bukkake_udon.png -I 02:06:26
HTTP/2 451
content-length: 0
server: CloudFront
date: Thu, 06 Feb 2020 17:13:29 GMT
しっかり、リターンコードが451となって返ってきていますね。
うどんは見えなくなってしまいました。
香川県からのアクセスをブロックする
block_region = "Tokyo"となっているところを"Kagawa"に書き換えます。
def lambda_handler(event, context):
...
- block_region = "Tokyo"
+ block_region = "Kagawa"
再度Lambdaをデプロイします。
$ lambroll deploy --region us-east-1
2020/02/07 02:07:05 [info] lambroll v0.3.3
2020/02/07 02:07:05 [info] starting deploy function block
2020/02/07 02:07:06 [info] creating zip archive from .
2020/02/07 02:07:06 [info] zip archive wrote 974 bytes
2020/02/07 02:07:06 [info] updating function configuration
2020/02/07 02:07:06 [info] updating function code
2020/02/07 02:07:07 [info] deployed version 2
2020/02/07 02:07:07 [info] updating alias set current to version 2
2020/02/07 02:07:07 [info] alias updated
2020/02/07 02:07:07 [info] completed
CloudFrontの設定も変更します。versionを 1 -> 2へ。
resource "aws_cloudfront_distribution" "web-hosting" {
...
default_cache_behavior {
lambda_function_association {
event_type = "viewer-request"
- lambda_arn = "arn:aws:lambda:us-east-1:XXXXXXXX:function:block:1"
+ lambda_arn = "arn:aws:lambda:us-east-1:XXXXXXXX:function:block:2"
}
おめでとうございます!!!これでKagawaからのアクセスをブロックすることができた(はず)です!!!!(香川県民の人、確認をおねがいします。)
その他補足事項とかツッコミとか
-
Lambda@Edgeのログはどこにでるの?
- リクエストを受け取ったCloudFrontのエッジサーバのあるリージョンです。
- 日本からのアクセスだと東京リージョンのCloudWatch Logsに出力されます。
-
本当にIPから特定できる場所があっているの?
-
IP DBINFOのデータが古ければ誤ってブロックしてしまうこともあります。常に正しいわけではないです。
-
リクエスト毎にIP DBINFOへのリクエストが走るよね?本番運用では現実的ではないのでは?
- そのとおりです。ただ、同じ手法でLambda内にIPデータベースをもたせるなどすれば外部への通信なしに制御することも可能かもしれないです。
-
条例での対象はインターネットではなく、コンピューターゲームだけど?
- 今回は試してないけど、CloudFrontのオリジンをALBにすればゲームなどのapiサーバーでも応用ができそうです
その他、気になることがあればコメントかTwitterまで!!