AWS Lambda@Edgeをつかって香川県からのアクセスをブロックする

# はじめに

この記事はネタ記事です。

本記事で紹介するような、特定の地域からのアクセスブロック等の行為を推奨するものでは一切ございません。

ただし、香川県のパブリックコメントへの回答によると

> Q：事業者の対策・協力義務について、具体的にはどのような対策を想定していますか。香川県からのアクセス遮断なども含まれますか。

> A：事業者に自主的な取り組みを呼びかけるものと考えています。

[【全22問】「1日60分までの根拠は」「なぜ議事録がないのか」――　香川県「ネット・ゲーム依存症対策条例（仮称）」、議会事務局との一問一答]( https://nlab.itmedia.co.jp/nl/articles/2002/06/news075.html)

との回答が得られており、万が一この条例が可決されれば、*事業者による自主的な取り組み*の一環として、香川県からのアクセスブロックを行う可能性は否定できません。

条例が可決された場合には、香川県からのアクセスをブロックすることが技術的に可能であるかを検証するという目的のもと、本記事を執筆いたしました。

# 概要

それではどうやって実現しましょう。

- [もしものために，香川県からのアクセスを避ける
](https://qiita.com/ni__no13/items/b1b215e7185920cd075c)

では、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とは

- [公式ドキュメント](https://aws.amazon.com/jp/lambda/edge/)

簡単に言えばCloudFrontのエッジサーバ上でLambda Functionを実行することができる機能です。
フックとなるトリガーには下図のイベントが用意されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581068/e2c709e2-1e93-0d44-c3ce-2d49b191578a.png)

今回は、"Viewer request"をトリガーにすればLambda@Edgeでアクセスブロックを実現できそうですね。

# Lambdaのコードを書く

Lambda＠EdgeはNodejsとPython3.7しか対応していないので、どちらか好みの方で書きます。Python3を用いてコードを書きました。

``` block.py
#!/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"
}
```
できたバケットには適当な画像、香川なんでうどんでもおいておきましょう。


![bukkake_udon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581068/54ddcc6b-4088-2593-54e1-62b0fa887bbc.png)



```
$ 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のアドレスから先程のうどんが見えるかどうかを確認しましょう。

![Screenshot from 2020-02-07 02-02-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581068/2a32b72b-c1d0-c406-f02d-c85caf4a4512.png)

ちゃんとうどんが見えますね。


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になるまで気長に待ちます。
ぐるぐるが終わったら先程のようにブラウザでアクセスしてみましょう。

![Screenshot from 2020-02-07 02-17-18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581068/9b077377-6ed7-480d-c73c-a5ac4ddb7ac2.png)


```
$ 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"に書き換えます。

```block.py

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](https://twitter.com/418iamteapot)まで！！