バックエンド側 node.js LocalStack APIGateway Lambda PDF作成&ダウンロード

パクリ24発目!
パクリ元
内容
以下一覧を完了させて ECS リリースさせることでバックエンド側を終わりとする。順序は適宜入れ替える。
- 開発環境構築
- プロジェクト作成・最適なフォルダ構成
- 認証チェック(含めた前処理)
- ログ処理
- APIによってはIPチェックやHeaderチェック
- バリデーション
- 環境ファイルによる振り分け
- DB参照・更新
- 外部API呼び出し
- 外部API呼び出し待ち合わせ
- 認証・認可
- SPA における CSRF CORS
- 長時間処理を非同期処理で
- メール送信
- 画像アップロード
- 画像ダウンロード
EXCEL/PDF作成- テスト
- (AWS環境デプロイ)
- Docker 化確認
- API Gateway+Lambda ←←← 今回ココ
- CodePipeline ←←← 途中まで完了(テストとマイグレーション残り)
- RDS 対応
- CloudFront
API Gateway+Lambda
前回途中で pending 状態に陥ってしまったのでちょっと別のを。PDF 作成というのを飛ばしていたのでここに再度入れてみた。なるべく関係者少なくということで S3 さんの登場は控えてもらった。
開発環境構築 docker
Lambda 開発するのはどうすんだ、ということでいろいろ確認したら docker-lambda というものやら LocalStack というものやらあってよく分からんが、周回遅れ的にはいろんな「試してみた」系記事の更新日時が新しいモノこそ神!LocalStack に決定。とうとう node 環境に docker を入れることになったか。docker も新しいの出てそうだしパクリ元1つ目で。
~$ echo "[boot]
systemd=true" | sudo tee /etc/wsl.conf
PS C:\Users\user > wsl.exe --shutdown
~$ sudo mkdir -p /etc/apt/keyrings
~$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
~$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
~$ sudo apt update
~$ sudo apt install -y docker-ce docker-compose-plugin
~$ sudo systemctl enable docker
~$ sudo usermod ubuntu -aG docker
開発環境構築 node
そういえば npm はグローバルで入れたので入ってる。今回 TypeScript は省略。
~$ mkdir lambdatest
~$ cd lambdatest/
~/lambdatest$ npm init -y
開発環境構築 LocalStack
パクリ元2つ目を参考にする。
~/lambdatest$ vi docker-compose.yml
version: "3.8"
services:
localstack:
container_name: localstack
image: localstack/localstack
ports:
- "127.0.0.1:4566:4566"
- "127.0.0.1:4510-4559:4510-4559"
environment:
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
~/lambdatest$ docker compose up -d
S3とかでなくただ動かしたいだけなので、パクリ元3つ目を参考にしようとしたら awscli と awscli-local が欲しいそうだ。
開発環境構築 awscli
パクリ元4つ目を参考に awscli を入れる。また、zip/unzip も入れる。
~/lambdatest$ sudo apt install zip unzip
~/lambdatest$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"
~/lambdatest$ unzip /tmp/awscliv2.zip -d /tmp/ && sudo /tmp/aws/install -i /usr/local/aws-cli -b /usr/local/bin
~/lambdatest$ rm /tmp/awscliv2.zip /tmp/aws -rf
開発環境構築 awscli-local
パクリ元5つ目を参考に awscli-local を入れる。
~/lambdatest$ sudo apt -y install python3-pip
~/lambdatest$ pip3 install awscli-local
~/lambdatest$ echo 'export PATH="$PATH:~/.local/bin"' >> ~/.bashrc
~/lambdatest$ source ~/.bashrc
Lambdaテスト用ソース作成
~/lambdatest$ vi sample.js
var response = {
statusCode: 200,
headers: {},
body: "Hello world!"
};
exports.handler = function(event, context, callback){
callback(null, response);
};
ローカルLambda関数登録&実行
パクリ元3つ目の手順通りに zip 圧縮、登録、実行と行う。
~/lambdatest$ zip sample.zip sample.js
~/lambdatest$ awslocal lambda create-function --function-name my-function --zip-file fileb://sample.zip --handler sample.handler --runtime nodejs18.x --role arn:aws:iam::123456789012:role/lambda-ex
~/lambdatest$ awslocal lambda invoke --function-name my-function out
~/lambdatest$ cat out
{"statusCode":200,"headers":{},"body":"hello world!"}
とりあえず動いた。途中、「An error occurred (ResourceConflictException) when calling the Invoke operation: The operation cannot be performed at this time. The function is currently in the following state: Pending」と出た時は少し待って対応。state が Failed だった時は docker-compose.yml を他のものに変えたりして対応。
ローカルAPI Gateway 作成&実行
パクリ元6つ目を参考にする。
~/lambdatest$ awslocal apigateway create-rest-api --name 'Sample API'
{"id": "★★★",・・・}
~/lambdatest$ awslocal apigateway get-resources --rest-api-id ★★★
{"items": [{"id": "◇◇◇",・・・}
~/lambdatest$ awslocal apigateway create-resource --rest-api-id ★★★ --parent-id ◇◇◇ --path-part sample
{"id": "▲▲▲", "parentId": "◇◇◇", "pathPart": "sample", "path": "/sample"}
~/lambdatest$ awslocal apigateway put-method --rest-api-id ★★★ --resource-id ▲▲▲ --http-method GET --authorization-type "NONE"
{"httpMethod": "GET", "authorizationType": "NONE", "apiKeyRequired": false}
~/lambdatest$ awslocal apigateway put-integration --rest-api-id ★★★ --resource-id ▲▲▲ --http-method GET --type AWS_PROXY --integration-http-method POST --uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:my-function/invocations --passthrough-behavior WHEN_NO_MATCH
{"type": "AWS_PROXY",・・・}
~/lambdatest$ awslocal apigateway create-deployment --rest-api-id ★★★ --stage-name dev
{"id": "ua4c8a7t71", "createdDate": "2023-11-06T19:44:22+09:00"}
~/lambdatest$ curl http://localhost:4566/restapis/★★★/dev/_user_request_/sample
Hello world!
API Gateway は機械的に実行すればいいだけのよう。ブラウザからのアクセスも問題なかった。
PDF 生成ソース作成
パクリ元7つ目を参考にまずは express で動くソース作成。
import * as puppeteer from 'puppeteer';
app.get('/', async (req: Request, res: Response) => {
const generatePDF = async () => {
const htmlTemplate = '<html><body style="margin:1em 10em 2em;"><div style="text-align:center">領収書</div><br><div style="text-align:left">NAME 様</div><br/><div style="text-align:right">PRICE 円</div></body></html>';
const htmlTemplate2 = htmlTemplate.replace("NAME", req.query.name as string).replace("PRICE", req.query.price as string);
//const browser = await puppeteer.launch({ headless: true })
const browser = await puppeteer.launch({executablePath: '/usr/bin/chromium-browser'});
const page = await browser.newPage();
await page.addStyleTag({content: `body {font-family: "IPA Pゴシック","IPA PGothic" !important;}`});
await page.setContent(htmlTemplate2);
const buffer = await page.pdf({printBackground: true, format: 'A4', margin: {top: 0, right: 0, bottom: 0, left: 0}});
browser.close();
return buffer;
}
const content = await generatePDF();
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", 'attachment; filename="test.pdf"');
res.send(content);
})
「Error: Failed to launch the browser process!」が出まくって全く動かなかったが、パクリ元8つ目によるとWSL内にブラウザがないためとのこと、加えてフォントもインストールする。
sudo apt-get install -y chromium-browser fonts-ipafont-gothic fonts-ipafont-mincho
ブラウザ経由で問題なくPDFがダウンロードされたのでOK。
ローカルLambda用puppeteer環境作成
パクリ元9つ目を参考にして、chromium と フォントのレイヤーを作成する。LayerVersionArn は後で使用する。何やら Lambda にデプロイする最大サイズがあるので、超えそうな場合は本体から切り離して共通ライブラリとして Lambda Layer にデプロイするのが筋なんだと。
~/lambdatest$ awslocal s3 mb s3://some-bucket
~/lambdatest$ git clone --depth=1 https://github.com/sparticuz/chromium.git && \
cd chromium && \
make chromium.zip && \
bucketName="some-bucket" && \
versionNumber="107" && \
awslocal s3 cp chromium.zip "s3://${bucketName}/chromiumLayers/chromium${versionNumber}.zip" && \
awslocal lambda publish-layer-version \
--layer-name chromium \
--description "Chromium v${versionNumber}" \
--content "S3Bucket=${bucketName},S3Key=chromiumLayers/chromium${versionNumber}.zip" \
--compatible-runtimes nodejs18.x \
--compatible-architectures x86_64
・・・
"LayerVersionArn": "arn:aws:lambda:us-east-1:000000000000:layer:chromium:1",
・・・
~/lambdatest$ mkdir fonts && \
cd fonts && \
curl -O https://moji.or.jp/wp-content/ipafont/IPAexfont/IPAexfont00401.zip && \
unzip IPAexfont00401.zip && \
mkdir .fonts && \
cp IPAexfont00401/*.ttf .fonts/ && \
zip -r fonts .fonts && \
awslocal lambda publish-layer-version \
--layer-name japanese-fonts \
--description "japanese-fonts" \
--zip-file fileb://fonts.zip \
--compatible-runtimes nodejs18.x \
--compatible-architectures x86_64
・・・
"LayerVersionArn": "arn:aws:lambda:us-east-1:000000000000:layer:japanese-fonts:1",
・・・
合わせて 先ほどの PDF 出力機能を Lambda 関数に置き換える。
const chromium = require("@sparticuz/chromium");
const puppeteer = require("puppeteer-core");
exports.handler = async function(event, context, callback){
const generatePDF = async () => {
const htmlTemplate = '<html><body style="margin:1em 10em 2em;"><div style="text-align:center">領収書</div><br><div style="text-align:left">NAME 様</div><br/><div style="text-align:right">PRICE 円</div></body></html>';
const htmlTemplate2 = htmlTemplate.replace("NAME", event.pathParameters.name).replace("PRICE", event.pathParameters.price);
const browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
await page.addStyleTag({content: `body {font-family: "IPA Pゴシック","IPA PGothic" !important;}`});
await page.setContent(htmlTemplate2);
const buffer = await page.pdf({printBackground: true, format: 'A4', margin: {top: 0, right: 0, bottom: 0, left: 0}});
browser.close();
return buffer;
}
const content = await generatePDF()
callback(null, content);
};
さきほど ローカル Lambda 上に作った my-function を削除して同じ名前で作成すれば API Gateway そのまま使えるのかな?my-function 削除、zip 圧縮、登録、レイヤーの登録とやって実行させると
~/lambdatest$ awslocal lambda delete-function --function-name my-function
~/lambdatest$ zip createReceipt.zip createReceipt.js
~/lambdatest$ awslocal lambda create-function --function-name my-function --zip-file fileb://createReceipt.zip --handler createReceipt.handler --runtime nodejs18.x --role arn:aws:iam::123456789012:role/lambda-ex
~/lambdatest$ awslocal lambda update-function-configuration \
--function-name my-function \
--layers \
"arn:aws:lambda:us-east-1:000000000000:layer:chromium:1" \
"arn:aws:lambda:us-east-1:000000000000:layer:japanese-fonts:1"
~/lambdatest$ awslocal lambda invoke --function-name my-function out
{
"StatusCode": 200,
"FunctionError": "Unhandled",
"ExecutedVersion": "$LATEST"
}
~/lambdatest$ cat out
{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'puppeteer-core'・・・
うーん…レイヤーにあるのが見えてない。今回のハマりどころ。パクリ元10個目のように、sparticuz/chromium.git でなく Sparticuz/chrome-aws-lambda.git の方がいいのかといろいろ変えてみたりしたのだが全くダメ。リンクされてるよなー、、と確認。
~/lambdatest$ awslocal lambda list-functions --function-version ALL
"Functions": [
{
"FunctionName": "my-function",
・・・
"Layers": [
{"Arn": "arn:aws:lambda:us-east-1:000000000000:layer:chrome-aws-lambda:1", "CodeSize": 54175592},
{"Arn": "arn:aws:lambda:us-east-1:000000000000:layer:japanese-fonts:1", "CodeSize": 9730611}
・・・
問題ないはずなんだがな。パクリ元11個目によると AWS 本体の Lambda では「検索するともろもろzipで固めてアップロードみたいな形が多いのですが公式にLambda Layerのarnが公開されています。」とあるので時代は動いているのだろう。周回遅れ的には時の流れを感じるこの瞬間。ローカルで確認できないなら本番デプロイしてみようか、と思っていたところ大事な記事発見。パクリ元12個目。
LacalStackでLambda Layerを使うにはPro版以上が必要だった・・・
ハマるわけですな。一応元記事パクリ元13個目も参照。
Lambda layers lets you include additional code and dependencies in your Lambda functions. With LocalStack Pro/Team, you can deploy Lambda Layers locally to streamline your development and testing process. Community users can still create, update, and list Lambda layers. However, the layers are not applied when invoking a Lambda function.
まあローカルでは完全にムリでしたと。Lambda に移してやってみる。
AWS Lambda
「createReceiptTest」関数を Node.js 18.x で作成。


さっき作った関数を貼り付け。

パクリ元11個目にあったレイヤーを指定する。

フォントのレイヤーはもう無視する。

「createReceiptEventTest」テストイベントを作成する。

テストを実行するとエラー。index.mjs というファイル名を index.js に変更。


またエラー。タイムアウトを3秒にしているからだそう。3分に変更する。




またエラー。今回のハマりどころ。パクリ元14個目は幾分古いが Node のバージョン落としてみ、というものがあったので Node.js 16.x に変更。


違うエラーに変わった。「runtime exited with error: signal: killed」というのはパクリ元15個目によるとメモリエラーらしい。分かりにくい。

メモリを 1024MB に変更。

お、うまくいった。

AWS API Gateway
パクリ元16個目を参考に API Gateway 側、REST API 作成。




GET、Lambda 関数、Lambda プロキシ統合、先ほどのLambda関数を指定してメソッド作成。


Lambda 側にも API Gateway が登場。

デプロイしてしまう。


API の設定でバイナリメディアタイプ「application/pdf」を追加。


URL を呼び出す。エラー。

Lambda ソース内で queryStringParameters と pathParameters を間違えてたのを修正したり、レスポンスヘッダとして application/pdf を追加したり、base64 エンコードしたりと足りない箇所だらけだった。さらに全部修正してブラウザからファイル保存ができるようになってからも PDF エラーがでて長かった。ただこれは Accept ヘッダがなかったせいで base64 デコードが行われていないだけだった。統合レスポンスのマッピングテンプレートの箇所とか結構ハマったぞ!
~/lambdatest$ curl "https://tdqytxi5l2.execute-api.ap-northeast-1.amazonaws.com/test?name=Mr.John&price=700" -H "Accept: application/pdf" --output test.pdf
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 5309 100 5309 0 0 6797 0 --:--:-- --:--:-- --:--:-- 6789

日本語フォント入れてないから日本語全部消えてるけどオールオッケー!
後で CloudFront を API Gateway の前に入れて Accept ヘッダに application/pdf 指定したらブラウザからも問題なくダウンロードできた。
雑感
毎回ハマるハマる。
ディスカッション
コメント一覧
まだ、コメントがありません