ローカルで動くから何?
画像の base64 エンコードデータを受け取って、sharp
モジュールを使ってリサイズし、base64 エンコードされたデータを返す API を作りたい。今回はプラットフォームが Azure なので、Function App を利用する。なお、App Service では、リリースは可能だがメモリ不足で動かないので、Function App に切り出した。
ローカルではあっさり動いたが、デプロイはすんなり行かなかった。
- デプロイは成功したが、成功してなかった
- デプロイは成功したが、動かなかった
GitHub Actions を使いまくっていたら、容量オーバーと言われたので、今回は全ての操作を Azure CLI で行う。Azure Portal でやっても良いのだが、なんとなく全部コマンドにしたかった。
開発環境
- Chip: Apple M2 Max
- macOS: 14.6.1
- node: v20.17.0
- npm: 10.8.2
- az: 2.64.0
- func: 4.0.6280
Azure のセットアップ
基本的には、「クイックスタート: コマンド ラインから Azure に TypeScript 関数を作成する」に従う。
az login
リソースグループの作成
az group create --name SharpResize-rg --location southeastasia
location
は、私がベトナム向けに作ったアプリケーションで利用するので、southeastasia になっている。
ストレージアカウントの作成
Function App のデプロイで使うため、作成が必要。
az storage account create \
--name sharpresizesa \
--location southeastasia \
--resource-group SharpResize-rg \
--sku Standard_LRS \
--allow-blob-public-access false
ストレージアカウント名には、大文字や記号は使えない。
sku は、一番料金がかからないものにした。他には以下のようなものがあるそうだが、デプロイにしか使わないので、最低限で良い。
- LRS: 最も安価。標準のストレージオプションであり、1GBあたり数セント程度。
- ZRS: LRSよりも少し高価ですが、データセンター障害に耐える可用性を提供します。
- GRS/RA-GRS: LRSの約2倍のコスト。地理的な冗長性を持つために高額です。
- GZRS/RA-GZRS: GRS/RA-GRSよりさらに高額で、最も高い冗長性と可用性を提供します。
Function App の作成
az function app create \
--resource-group SharpResize-rg \
--comsumption-plan-location southeastasia \
--runtime node \
--runtime-version 20 \
--functions-version 4 \
--name sharp-resize \
--storage-account sharpresizesa
--os-type linux
とすると、OS が Linux になり気分が良いが、あれこれ使えず Debug が大変なので、無指定で Windows で動かすことにした。
node でサポートされているバージョンは、18 と 20 のようだが、積極的に18を選ぶ理由はないかと。
これで、Azure の設定は、後で環境変数を設定するが、いったんおしまい。
Azure Portal で確認してみると、Function App, Storage Account 以外に Action Group, Application Insights, App Service Plan が作成されている。全部必要なので、消さないように。
API の作成
func init --typescript
func new --name resize --template "HTTP trigger" --authlevel "anonymous"
これが終わると、
├── host.json
├── local.settings.json
├── node_modules
├── package-lock.json
├── package.json
├── src
│ └── functions
│ └── resize.ts
└── tsconfig.json
こんなような感じになる。以前は、
└── resize
├── index.ts
└── function.json
というような形になっていたが、function.json は、いまは必要ない。うまく行かないときに Chat GPT に聞くと、function.json がないせいだと言われるが、必要ない。
sharp
をインストールする。
npm install sharp
コマンドで出来上がったテンプレートを元に GitHub Copilot に sharp を使った resize を問答を繰り返し書かせる。
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
const sharp = require('sharp');
export async function resize(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
context.log(`Http function processed request for url "${request.url}"`);
const quality: number = 90;
const width: number = 1920;
const fit: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' | 'containe' =
'inside';
const withoutEnlargement: boolean = true;
// get imageData from post params
let imageData: any;
try {
const body: any = await request.json();
imageData = body.imageData;
} catch (error) {
context.log('Error parsing JSON body', error);
return {
status: 400,
body: 'Invalid JSON body',
};
}
const blob = atob(imageData.replace(/^.*,/, ''));
let buffer = new Uint8Array(blob.length);
for (let i = 0; i < blob.length; i++) {
buffer[i] = blob.charCodeAt(i);
}
// check buffer
const imageBuffer = Buffer.from(buffer);
if (!Buffer.isBuffer(imageBuffer)) {
return {
status: 500,
body: 'Expected image buffer to be a Buffer.',
};
}
const resizedImageBuffer = await sharp(imageBuffer)
.resize({
width,
fit,
withoutEnlargement,
})
.webp({ quality })
.toBuffer();
return {
status: 200,
body: JSON.stringify({
imageData:
'data:image/webp;base64,' + resizedImageBuffer.toString('base64'),
}),
headers: {
'Content-Type': 'application/json',
},
};
}
app.http('resize', {
methods: ['POST'],
authLevel: 'anonymous',
handler: resize,
});
良さそうである。quality, width, fit, withoutEnlargement などはリクエストパラメータとして受け取った方が汎用的だが、今回は変える必要がないので、固定値にした。出力は、webp になっている。用途に合わせ好きに設定すれば良い。
npm start
とすると、build が走った後、動く。
[2024-09-17T10:36:14.398Z] Worker process started and initialized.
Functions:
resize: [POST] http://localhost:7071/api/resize
正しく動作することが確認できたので、デプロイする。
npm run build
func azure functionapp publish sharp-resize
こうすると、デプロイはすぐに完了するが…
...
Deployment successful.
Remote build succeeded!
Syncing triggers...
Functions in sharp-resize:
あれ? resize がいない。Azure Portal で確認してもない。URL を叩いてみても当然 404。
正しくデプロイされると、
Functions in sharp-resize:
resize - [httpTrigger]
Invoke url: https://sharp-resize.azurewebsites.net/api/resize
こうなるハズである。何故なのか?
API を Azure 上で動くようにする
Function を認識させる
ローカルでは動くし、デプロイも成功するが、デプロイ先にモノがないので、ログもない。ただ動かない。Debug 難易度の高い案件だ。Chat GPT や GitHub Copilot に助けを求めると、ログ見ろ、function.json がないからだなどと言われるが、どれも的外れ。
途方に暮れるが、可能性をひとつずつ潰していくしかない。
まずは、func new --name resize --template "HTTP trigger" --authlevel "anonymous"
で生成される Hello, world! な関数だけにしてデプロイしてみる。
なんの問題もなくデプロイされる。なので、function.json
が〜…な線は消えた。ということは、ソースコードに問題があるハズ。
処理の塊ごとに、コメントアウトしながら、デプロイを繰り返し丁寧に確認していく。
ようやくたどり着いた犯人は、const sharp = require('sharp');
だった。
import {...} from '@azure/functions';
const sharp = require('sharp');
export async function resize(...) {
...
const resizedImageBuffer = await sharp(imageBuffer)...
...
}
...
この sharp
が関数内で認識できず、不良関数として落ちていた。
import {...} from '@azure/functions';
export async function resize(...) {
...
const sharp = require('sharp');
const resizedImageBuffer = await sharp(imageBuffer)...
...
}
...
結局、こうすることで、デプロイしたときに関数として認識されるようになった。もちろん、ローカルでも動く。めでたし。
デプロイした API を叩いてみると、
500 Internal Server Error
めでたくなかった。だがしかし、今回はログがある!
sharp のせいだった
今回、500 エラーを意図的に吐き出しているのは、Buffer のチェック部分だけだ。リクエストが何かダメだった?と思いつつ、ログを見てみる。
そもそも API が呼び出されると、
context.log(`Http function processed request for url "${request.url}"`);
によって、呼び出されたことが吐き出されるのだが、いない!!なんでだよ…
API が呼び出されたが、処理に入る前に 500 エラー。API を認識させるのに、かなり手間取ったので、ようやく解決できた後にこれは絶望感が深い。
だが、ログは出るはずだ。落ち着け。
ログを監視しながら、500 エラーを起こし続けると、犯人が判明した。
Could not load the "sharp" module using the win32-ia32 runtimePossible solutions:
- Ensure optional dependencies can be installed:
npm install --include=optional sharp
- Ensure your package manager supports multi-platform installation:
See https://sharp.pixelplumbing.com/install#cross-platform
- Add platform-specific dependencies:
npm install --os=win32 --cpu=ia32 sharp
- Consult the installation documentation:
See https://sharp.pixelplumbing.com/installStack
あぁ、あなたですか、sharp。確かに画像をいじるんですもんね。プラットフォームに依存しますよね。Windows で開発してたらもしかしたら遭遇しなかったかも知れないですね。
npm remove sharp
npm install --os=win32 --cpu=ia32 sharp
npm run build
func azure functionapp publish sharp-resize
これで、関数は認識され、正しく動くようになった。今度こそ、めでたし。
win32-ia32 は、az function app create ...
した時に、--os-type linux
とした場合は、異なるので、自分のプラットフォームに合わせたものを指定する必要がある。なお、私のローカル(Apple シリコンの Mac)の場合、darwin-arm64 になる。
だが不便である
正しくデプロイできるようにはなったが、ローカルではもちろん動かない。いちいち覚えてられないし、package.json
に忘れないように書いておく。
{
"scripts": {
"build": "echo \"run localbuild or prodbuild instead.\" && npm run localbuild",
"localbuild": "npm run clean && npm remove sharp && npm install sharp && tsc",
"prodbuild": "npm run clean && npm remove sharp && npm install --os=win32 --cpu=ia32 sharp && tsc"
...
},
}
localbuild
, prodbuild
を足した。build
は、start
などで呼び出されるため、なんらか build する必要があるため、localbuild を呼ぶことにした。今回の流れでは、ローカルで build するが、そうでない場合は、その環境に依存したパッケージがインストールされることだろう。注意喚起のため、メッセージも出るようにしておいた。
なお、試していないが、GitHub Actions でデプロイする場合、build は GitHub の Ubuntu 上で行われるので、build のステップで、prodbuild するか、まぁ何か工夫が必要になると思われる。
クリーンアップ
ただ手を動かして試してみたかったという人は、下記コマンドで今回つくったものを一括で削除できる。
az group delete --name SharpResize-rg
まとめ
sharp
注意!
例えば、あなたがリリース担当で、メンバーが開発を行っているような場合、今回のようなケースは、解消が難しそうだ。ローカルでは動く!と何度も耳にしそう。役割分担で言えば、インフラ構築を担当した人が、OS は Windows だよと申し送りするべきなのかも知れないが、一度困らないと気をつけられない。AI が指摘してくれると良いなぁ。こんなに頻繁に行われそうな内容なのに、ドンピシャなドキュメントも見当たらなかったので、備忘録として。