AWS Lambdaをつかって大量のリンク切れ有無をチェックする

# 概要
* AWS Lambdaでリンクチェッカーを作った
* urlのリンクリストのcsvファイルをインプットにしてリンクチェック結果をDynamoDBに出力します

[![https://gyazo.com/71cb6ee6479a2832ef45d6bc39d9b687](https://i.gyazo.com/71cb6ee6479a2832ef45d6bc39d9b687.png)](https://gyazo.com/71cb6ee6479a2832ef45d6bc39d9b687)


# ざっくりとした技術説明
1. rubyでS3にidとurlキーを持つjsonを出力
2. S3へのput出力をトリガーとしてlambda関数を実行してリンク切れを有無をstats_codeとして取得
3. 取得した結果をDnamoDBへ出力

# 手順
## S3のバケットを作成する
バケットを作成する。ここでは__url-checker__という名前で作りました。以下のスクリプトでは、バケット名はurl_checkerを前提にしていますので、適宜書き換えてください。

1. url-checkerバケットを作成
2. __url__というフォルダを作成

[![https://gyazo.com/7b097c2f5f0d5b2244361863a54ad4ec](https://i.gyazo.com/7b097c2f5f0d5b2244361863a54ad4ec.png)](https://gyazo.com/7b097c2f5f0d5b2244361863a54ad4ec)

## DynamoDBを作成する
__url-checker__という名前でDynamoDBを作る。プライマリキーは__url__です。

[![https://gyazo.com/deef0dc0d45d742a4ef12ffe9f062d9c](https://i.gyazo.com/deef0dc0d45d742a4ef12ffe9f062d9c.png)](https://gyazo.com/deef0dc0d45d742a4ef12ffe9f062d9c)

[![https://gyazo.com/af61b213056a495279e052072856b4e5](https://i.gyazo.com/af61b213056a495279e052072856b4e5.png)](https://gyazo.com/af61b213056a495279e052072856b4e5)

書き込みはデフォルトの5です。エラーが起きるようでしたら書き込み数値を適当に上げてください。（料金に注意！！！！！）

## Lambdaファンクションを作成する
* Lambdaファンクションを作成する。ファンクション名はなんでもいいです。
    * S3ファイル（jsonファイル）からidとurlを読み込んで、リンクをチェックし、DyanmoDBへ保存しています。
* トリガーはS3のurl-checkerバケットのurlフォルダのputを指定してください。
* DynamoDBへのアクセスを許可してください。

[![https://gyazo.com/e6d9d984e550c07434de37ea08fced6e](https://i.gyazo.com/e6d9d984e550c07434de37ea08fced6e.png)](https://gyazo.com/e6d9d984e550c07434de37ea08fced6e)

タイムアウトしないように。lambdaのタイムアウトは5分に伸ばした。

[![https://gyazo.com/efbd901d6921aa6ee92235187b1b9205](https://i.gyazo.com/efbd901d6921aa6ee92235187b1b9205.png)](https://gyazo.com/efbd901d6921aa6ee92235187b1b9205)



* 関数は以下の通りです。


```lambda_function.py
import urllib.request
import json
import boto3
import random
from time import sleep

error_code = 0  # エラーの場合は0を返す
dynamodb_table = 'url-checker'  # 事前に作成したdynamodbのテーブル名
max_wait_sec = 120

def lambda_handler(event, context):
    wait_sec = random.randint(1, max_wait_sec)
    sleep(wait_sec) # 同一URLへの同時アクセスする可能性を減らすため念のためアクセス時間をずらす
    site = get_s3_json(event)
    site["status_code"] = get_status_code(site['url']) # status codeをチェックして結果をjsonで返す
    print(site)
    put_dynamo_table(site) # 結果をdynamoDBへ保存する
    return site


def put_dynamo_table(item):
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(dynamodb_table)
    table.put_item(Item=item)


def get_s3_object(event):
    s3 = boto3.client('s3')
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    return s3.get_object(Bucket=bucket, Key=key)['Body'].read()


def get_s3_json(event):
    s3_object = get_s3_object(event)
    return json.loads(s3_object)


def get_urllib_response(url):
    req = urllib.request.Request(url)
    return urllib.request.urlopen(req)


def get_status_code(url):
    status_code = 0
    try:
        with get_urllib_response(url) as res:
            print(res.status)
            status_code = res.status
    except urllib.error.HTTPError as err:
        print(err.code)
        status_code = err.code
    except urllib.error.URLError as err:
        print (err.reason)
        status_code = error_code

    return status_code

```

## S3にトリガー（jsonファイル）をアップロードする
今回作成したlambda関数は、S3へのputをトリガーにしています。

トリガーを発火させるため、インプットCSVファイルからidとurlを読み込んで、jsonファイルとしてS3へ書き出すためのスクリプトをrubyで作成しました。

なお、AWSのアクセスキーとAWSのシークレットアクセスキーは各自のモノを書き込んで使ってください。

aws-sdk-s3は事前にインストールしておいてください。

status code取得のためのメソッド箇所は[Python の HTTP クライアントは urllib.request で十分](https://qiita.com/hoto17296/items/8fcf55cc6cd823a18217#%E4%BE%8B%E5%A4%96%E5%87%A6%E7%90%86)を使いました。

```execute_que.rb
require 'aws-sdk-s3'
require 'csv'

INPUT_PATH = ARGV[0] || 'test2.csv'
BUCKET = 'url-checker'
FOLDER = 'url'
AWS_ACCESS_KEY_ID = 'アクセスキー'
AWS_SECRET_ACCESS_KEY = 'シークレットアクセスキー'

def start(path: '', limit_url_duplication: true, limit_num: nil)
  s3_client = create_s3_client
  lists = get_lists(path)
  if limit_url_duplication && uniq_url?(lists)
    p 'URL is duplicated. Use uniq URLs in csv.'
    exit
  end

  lists.each_with_index do |row, index|
    filename = "#{row[:id]}.json"
    key = "#{FOLDER}/#{filename}"
    body = {id: row[:id].to_i, url: row[:url]}
    s3_client.put_object(
      bucket: BUCKET,
      key: key,
      body: body.to_json
    )
    break if index == limit_num
  end
end

def create_s3_client
  return Aws::S3::Client.new(access_key_id: AWS_ACCESS_KEY_ID, secret_access_key: AWS_SECRET_ACCESS_KEY)
end

def get_lists(path)
  lists = []
  CSV.foreach(path, headers: true) do |row|
    headers = row.headers
    lists << headers.each_with_object({}) do |c, h|
      h[c.to_sym] = row.field(c)
    end
  end
  return lists
end

def uniq_url?(lists)
  urls = lists.map {|list| list[:url]}
  !(lists.count == urls.uniq.count)
end

# csv作成ミスによる、同一URLへの複数回アクセスを防止するためlimit_url_duplicationの初期値はtrueです。
start(path: INPUT_PATH, limit_url_duplication: true)

```

# リンクチェックを実行する！
* インプットファイルを下記のように用意します。同一URLヘの同時アクセスを避けるため、urlは必ず固有にしてください。

```input.csv
id,url
1,https://www.AAAA.co.jp
2,https://www.BBBB.co.jp
```

* 下記のコマンドを実行します。

```console
> ruby execute_que.rb input.csv
```
S3のurl-checker/urlフォルダ内にjsonファイルが作成されます。
DynamoDBのurl-checkerテーブルにチェック結果が格納されます。

[![https://gyazo.com/71cb6ee6479a2832ef45d6bc39d9b687](https://i.gyazo.com/71cb6ee6479a2832ef45d6bc39d9b687.png)](https://gyazo.com/71cb6ee6479a2832ef45d6bc39d9b687)

以上です！

# 所感
Lambdaすごい便利。楽しい。1000台動いている姿をみると全能感に浸れる。