前回の続き
AWSだけならSAMがいいと言われていますがまだserverlessもちゃんと触ったことがなかったのでまずはserverlessで実装してみます
内容としては「LabmdaでHeadlessChrome(Puppeteer)を用いてスクレイピングして結果をSlackへ投稿する」です
スクレイピングの中身に関してはとくに触れません
具体的にやることは下記
- chromeのLayerプロジェクトの実装
- インストール
- deploy
- Lambdaでスクレイピング部分のプロジェクト実装
- Layerの参照
- スケジューリング設定
- 環境ごとの変数切り替え
- tokenの暗号化、複合化
- フォントの読み込み
- スクレイピング処理の実装
- デプロイ
それぞれ別のディレクトリを作成して別々にデプロイします
Layer用のプロジェクト
インストール
必要なものをインストールしたらデプロイします
今回Chromeのバイナリはchrome-aws-lambda
に入っている方を使うのでpuppeteerからはインストールしないようにします
- nodejs/.npmrc
puppeteer_skip_chromium_download=true
インストールします
cd nodejs npm install --save puppeteer serverless chrome-aws-lambda
- serverless.yml
service: puppeteer provider: name: aws runtime: nodejs8.10 stage: dev region: ap-northeast-1 package: exclude: - serverless.yml layers: modules: path: ./ name: modules description: app modules compatibleRuntimes: - nodejs8.10
- ディレクトリ構成
serverless.yml nodejs |-- node_modules ..... ..... ..... |-- package-lock.json `-- package.json
nodejs
ディレクトリを掘ってそこでnpmモジュールをインストールしています
こうするとLayer用のプロジェクトでインストールしたライブラリが/opt/nodejs/node_modules
以下で参照できるようになります
deploy
特別なことはせずにデプロイだけ
npx sls deploy
50Mギリギリのサイズになりました
ARNは後で使います
スクレイピング用のプロジェクト
次にスクレイピングする処理のLambdaを実装します
Layerの参照
同じservice内であればCloudFormationの書式にしたがってレイヤーのRefを指定することでよしなにレイヤー参照してくれるようになります
- serverless.yml
layers: test: path: layer functions: scraping: handler: handler.main layers: - {Ref: TestLambdaLayer}
1つの設定ファイル(もしくはプロジェクト)でLayerとメインのコードを書く場合はこちらの参照方法で行うのが良さそうです
今回serviceが違うので上記方法は使えないのでLayerのプロジェクトをデプロイしたときに出てくるARNをコピーして参照させます
- serverless.yml(抜粋)
functions: scraping: handler: handler.execute layers: - arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:layer:modules:5
ARNで記述するとバージョンが固定になってしまうのでLayerの方を更新したらこちらも更新する必要が出てきてしまいます
なのでできればCfnの書式のほうが良いでしょう
これでLayerに入れたライブラリが使用できるようになります
パスが通っているところに置いたのでいつもスクリプトを書く感じと同様にrequireで読み込めるようになっているはずです
スケジューリング設定
- serverless.yml(抜粋)
functions: scraping: handler: handler.execute events: - schedule: cron(0 1 ? * FRI *)
最初scheduleはcron式でいけるのか! ってことで楽勝だと思っていたらはまりました
ワイルドカードの使い方など通常のcrontabと微妙に違うところがあるようです
なのでまずドキュメントを読んだほうがよさそうです
ルールのスケジュール式 - Amazon CloudWatch Events docs.aws.amazon.com
cron 式の日フィールドと曜日フィールドを同時に指定することはできません。一方のフィールドに値 (または *) を指定する場合、もう一方のフィールドで ? (疑問符) を使用する必要があります。
ということで今回特定の曜日に実行したかったので曜日の指定をしたのですが日のフィールドに?
を指定しないといけませんでした
これ気づくまで結構時間かかってしまいました。。。自戒を込めてちゃんとドキュメントは読みましょうw
環境ごとの変数切り替え
両方のプロジェクトで言えることですがステージを定義することでデプロイ先を切り替える事ができます
- serverless.yml(抜粋)
provider: name: aws runtime: nodejs8.10 stage: ${opt:stage, self:custom.defaultStage} environment: SLACK_USER_ID: ${self:custom.slackUserId.${opt:stage, self:custom.defaultStage}} custom: defaultStage: dev slackUserId: dev: XXXXXXXX prod: YYYYYYYY
例だとprovider.environment.SLACK_USER_ID
を開発と本番で分けてます
実行時にstage
がdev
ならXXXXXXXX
に、prod
ならYYYYYYYY
へ通知が送られるようになります
これでカスタム変数にdev,prod両方のIDを記述してstageによって環境変数にわたす値を変える(通知先を変える)ということができるようになりました
スクリプト側からはconst token = process.env.SLACK_USER_ID;
という感じで取得できるようになります
tokenを暗号化、復号化して処理
SystemManagerのパラメータストア + KMSを使ってslackのAPI tokenを取得します
- SLACK_TOKENのパラメーターをコンソールから設定
とりあえず使ってみるというところを目的としたので今回はマネジメントコンソールから作成します
必要項目を入力して作成します
- serverlessから読み込み
デプロイ時のユーザーにパラメータストア、KMSに対する権限があれば読み込み、復号などができるので権限を付与して試してみます
- serverless.yml
provider: environment: SLACK_TOKEN: ${ssm:/lambda/slackToken~true}
タイプ: 安全な文字列を選択した場合は ~true
を付与して ${ssm:名前~true}
という形で読み込みます
serverlessの設定ではSLACK_TOKENの環境変数へ書き出すようにしてみます
まず権限がない場合
SSM経由でパラメーターを読めないときは下記のようにwarningが出ます
npx sls deploy Serverless Warning -------------------------------------- A valid SSM parameter to satisfy the declaration 'ssm:/lambda/slackToken~true' could not be found. Serverless: Packaging service...
これだけだと何が何だかわからないのでcliから確認します
- aws cliから確認
aws ssm get-parameters --names "/lambda/slackToken" An error occurred (AccessDeniedException) when calling the GetParameters operation: User: arn:aws:iam::xxxxxxxxxxxxxx:user/hoge is not authorized to perform: ssm:GetParameters on resource: arn:aws:ssm:ap-northeast-1:xxxxxxxxxxxxxx:parameter/lambda/slackToken
- 権限を付与
documentを見てインラインポリシーをアタッチしただけだとうまく行かなかったのでとりあえずSSM,KMSにある程度権限をもたせたらうまく行きました
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:DescribeParameters", "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParameters" ], "Resource": "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxxxxx:parameter/lambda/slackToken" }, { "Effect": "Allow", "Action": [ "kms:ListKeys", "kms:ListAliases", "kms:Describe*", "kms:Decrypt" ], "Resource": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxxxxx:key/*" } ] }
- 確認
aws ssm get-parameters --names "/lambda/slackToken" { "Parameters": [ { "Name": "/lambda/slackToken", "Type": "SecureString", "Value": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "Version": 1, "LastModifiedDate": 1554787408.812, "ARN": "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxxxxx:parameter/lambda/slackToken" } ], "InvalidParameters": [] }
パラメータストアから値がとれました
lambdaのdeploy,invokeもうまくいきました
リポジトリに残したくない処理などはパラメータストアを使うことで簡単に暗号化、復号化、使用することができるので気にすることが少なくなるので楽ですね
フォントの読み込み
chrome-aws-lambdaではフォントの読み込みまでサポートしてくれています
前回も紹介したが下記コードで日本語フォントに対応させることができます
- handler.js(抜粋)
const chromium = require('chrome-aws-lambda'); module.exports.execute = async (event, context, callback) => { await chromium.font('https://raw.githack.com/googlei18n/noto-cjk/master/NotoSansJP-Black.otf');
ここではフォントのURL指定でraw.githack.comを使っていますがおそらく公式のサービスではないはずなのでrawgit.comみたいにサービス終了のアナウンスがあったりしたら対応をしなくてはならなそうですね
それを踏まえてもまぁ便利ではあると感じます
実際の処理
実際の細かな処理は省きますが下記のようなコードでスクレイピングまで始めることができるようになりました
- handler.js(抜粋)
'use strict'; const puppeteer = require('puppeteer'); const chromium = require('chrome-aws-lambda'); module.exports.execute = async (event, context, callback) => { await chromium.font('https://raw.githack.com/googlei18n/noto-cjk/master/NotoSansJP-Black.otf'); const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-gpu', '--single-process'], executablePath: await chromium.executablePath, defaultViewport: chromium.defaultViewport, headless: true }); const page = await browser.newPage(); await page.goto('スクレイピング対象のURL'); ..... ..... ごにょごにょ ..... .....
- serverless.yml(抜粋)
package: exclude: - node_modules/**
今回はLayerからライブラリを読み込むようにするのでLambdaにあげないように除外設定を行います
本番のデプロイ
本番は--stage prod
をつけるだけです
npx sls deploy --stage prod
Layerも読み込めていそうですね
CloudwatchEventでスケジューリングもできていそうです
まとめ
思った以上にいろいろと時間がかかってしまいましたがなんとか納得行くところまで進めることができました
1回雛形を作ってしまえば流用できる部分が多いと思うのでしばらくこのパターンで色々作ってみたいなと思います