# DLsiteの新作をRSS配信する(Ruby on AWS Lambda + S3 + AWS SAM)

2020/05/02

毎日DLsiteの新作を見ているんですが、そういえばRSS配信してないのかなと思って調べた感じなかったので作りました。AWS LambdaとSAMの勉強も兼ねています。

# 構成

  • AWS SAMでAWS LambdaにRubyコードを依存gem含めてデプロイ
  • AWS CloudWatchから定期的にAWS Lambdaをキック
  • AWS LambdaからRSSを作成してパブリックアクセス可なS3にput
    • DLsiteの新作ページをクローリング&パース
    • パース結果をS3にjson形式でput
    • 前回のパース結果のjsonを取得し、差分を更新
    • RSSフィード形式に起こしてxmlドキュメントとしてS3にput

AWS LambdaはAWS SESに来たメールの転送に使っていたりするのですが、手作業で適当に作ってそれっきりで理解が浅かったので、この機会にちょっと勉強しました。

Lambdaの構成を再現できるよう、またNative ExtentionなgemをLambdaで使えるよう、AWS SAMで構成管理を行うようにしました。

リポジトリはここ。

dlsite_rss

RSSのURLはこちら。

voice_rss.xml

インフラ回りで時間食いすぎて肝心のRubyのロジックが相当ガバいのなんとかしたい。

# ハマったところ

# gemをLambdaで使いたいときはgemごとパッケージングしてアップロードする必要がある

docs.aws.amazon.com - Ruby の AWS Lambda デプロイパッケージ#追加の依存関係を使用して関数を更新する

Lambda実行前に勝手にbundle installするような方法はなく、依存gemごとコードをパッケージングしてLambdaにアップロードする必要があります。

※リンクではzipで固めてaws lambdaでアップロードしていますが、後述の理由でこの方法は使わず、samを使っています。

# Native ExtentionなgemはAmazonLinux上でコンパイルしてパッケージングする必要がある

nokogiriなど、Native Extentionなgem(RubyではなくCで書かれたもの)は、コンパイルした環境以外では動きません。なので、パッケージングする際に、Lambdaの実行環境と同じ環境でbundle installしてやる必要があります。

LambdaはAmazonLinux上で動いています(アーキテクチャはわからん)。コンテナでAmazonLinuxイメージを用意してそこでコンパイルしてもよかったのですが、sam build --use-containerで その操作をラップしてくれること、およびsamでLambdaの構成管理も行えるとのことなので、ここでsamを採用しました。

SAMの方式として、ローカルでコンテナ経由でLambda関数をパッケージング(sam build)し、それをS3にアップロード(sam package)し、それをLambdaにデプロイ(sam deploy)する、という流れを取ります。

$ pipenv run sam build --use-container
$ pipenv run sam package --s3-bucket ${SAM_S3_BUCKET}
$ pipenv run sam deploy \
  --stack-name ${SAM_STACK_NAME} \
  --s3-bucket ${SAM_S3_BUCKET} \
  --s3-prefix ${SAM_S3_PREFIX} \
  --capabilities ${SAM_CAPABILITIES} \
  --region ${SAM_REGION} \
  --no-fail-on-empty-changeset
1
2
3
4
5
6
7
8
9

なお、上記sam package/sam deployコマンドに必要な設定値(環境変数で指定している部分)は、一度sam buildした後にsam deploy --guidedコマンドを実行すると対話的にいろいろきかれ、samconfig.tomlに吐き出してくれるのでそれを使います。

samconfig.tomlファイルがカレントディレクトリに存在すれば、オプションなしのsam deployコマンドのみでLambdaにデプロイできますが、ファイルは取り回しが悪いので環境変数&コマンドに起こしています。

# Lambdaで使うGemfileとローカルでのテスト用に使うGemfileは分ける

これいまいち腑に落ちていないのですが、samはデフォルトでそういう構成を推奨しているようです。sam initをするとこうなります。

コンテナなどでかんぺきにdevelopment/test/production環境を分離できるなら分けなくてもいいと思うのですが、そうでない環境向けにこういう構成をとっているんですかね。


 





 








$ tree
├── Gemfile
├── Gemfile.lock
├── Pipfile
├── Pipfile.lock
├── README.md
├── dlsite_rss
│   ├── Gemfile
│   └── app.rb
├── events
│   └── event.json
├── template.yaml
└── tests
    └── unit
        └── test_handler.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

dlsite_rss/GemfileにはLambda上で使うgemを記載して、Gemfileではそれプラステスト用のgemを記載しています。

  • Gemfile






 




# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

eval_gemfile File.join( File.dirname(__FILE__), "dlsite_rss/Gemfile")
gem "test-unit"
gem "mocha"
gem "pry-byebug"
1
2
3
4
5
6
7
8
9
10

ローカルでユニットテストを行うときはこう。

$ bundle exec ruby tests/unit/test_handler.rb
1

samコマンドには、ローカル上にLambdaを模した環境を構築し、そこでLambda関数レベルのテストを行うこともできます(今回実装していないですが)

$ pipenv run build --use-container
$ pipenv run sam local invoke DlsiteRSSFunction
1
2

# オブジェクトACLでS3のパブリックアクセスを許可している場合、指定しないとオブジェクトが更新される度にパブリックアクセスがオフになる

完全にAWS初心者の感想です。そうなのかーという感じですがそうみたいです。更新するたびにaclを設定してやる必要があります。

def put_to_s3(key:, body:, content_type: "application/json; charset=utf-8", public: false)
  acl = public ? "public-read" : "private"

  s3_client.put_object(
    bucket: ENV['BUCKET'],
    key: key,
    body: body,
    content_type: content_type,
    acl: acl,
  )
end
1
2
3
4
5
6
7
8
9
10
11

# SAM(というかCloudFormation)の権限管理めんどくせ

8割方の時間はこれにもっていかれました。sam deployして権限足りてなくて怒られてまた追加して。。。と無限に繰り返していました。

# デプロイするユーザに必要な権限

SAMはバックエンドにCloudFormationを使っているそうです。なのでデプロイするAWSユーザにCloudFormationを操作する権限が必要です。

また、Lambdaの関数を作ったり削除したり設定を書き換えたりタグを打ったりするので、Lambdaを操作する権限も必要です。デプロイするユーザとLambdaを実行するユーザを分けていれば、デプロイするユーザにLambdaを実行する権限は不要です。

LambdaソースコードはS3経由でLambdaにアップロードされるので、S3の特定のバケットのオブジェクトを読み書きする権限も必要です。

Lambda実行ロールも読み書きするので、IAMのroleを操作する権限も必要です。

最後まで残っていたのがこのエラーメッセージ。

User: arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxx is not authorized to perform: events:PutRule on resource: arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:rule/**********-xxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxx (Service: AmazonCloudWatchEvents; Status Code: 400; Error Code: AccessDeniedException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
1

エラーメッセージをググると出るのですが、これはLambdaをCloudWatchからキックするためのEventBridgeのルール作成権限が足りていないことがエラーの原因です。

このへんの権限管理は手でIAMポリシーをポチポチやってしまったのですが、ゆくゆくは構成管理したい。

ちなみに現状デプロイユーザにアタッチされている権限一覧はこちら。もっとアクションもリソースも絞りたい。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:TagResource",
                "events:PutRule",
                "iam:CreateRole",
                "iam:AttachRolePolicy",
                "lambda:GetFunctionConfiguration",
                "cloudformation:CreateChangeSet",
                "lambda:UntagResource",
                "iam:PassRole",
                "cloudformation:DescribeStackEvents",
                "lambda:ListTags",
                "events:RemoveTargets",
                "lambda:DeleteFunction",
                "cloudformation:DescribeChangeSet",
                "cloudformation:ExecuteChangeSet",
                "iam:GetRole",
                "events:DescribeRule",
                "s3:*",
                "lambda:UpdateFunctionConfiguration",
                "cloudformation:GetTemplateSummary",
                "cloudformation:DescribeStacks",
                "events:DeleteRule",
                "events:PutTargets",
                "lambda:UpdateFunctionCode",
                "lambda:AddPermission",
                "cloudformation:DescribeStackSet",
                "lambda:RemovePermission"
            ],
            "Resource": "*"
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# LambdaをCloudWatchから定期実行できるようにする

先ほどCloudWatchからLambdaをキックするルールをデプロイ時に作成するために、EventBridgeのルール作成権限をデプロイするユーザに付与しました。

それとは別に、このLambda関数をEventBridgeからキックしてもいいというパーミッションをLambda関数に与えてあげる必要があります。これはAWS SAMで作りました。

  • template.yaml
Resources:
  (~snip~)
  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref "DlsiteRSSFunction"
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
1
2
3
4
5
6
7
8

AWSの権限管理は本当によくわからん。。。

# 所感

新しいことをもっとさくっとできるようになりたい。いつまで経っても手が遅い。