0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Azure Function App に sharp で 画像リサイズする API をデプロイする

Last updated at Posted at 2024-10-09

ローカルで動くから何?

画像の 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 を問答を繰り返し書かせる。

src/functions/resize.ts
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'); だった。

src/functions/resize.ts
import {...} from '@azure/functions';
const sharp = require('sharp');

export async function resize(...) {
  ...
  const resizedImageBuffer = await sharp(imageBuffer)...
  ...
}
...

この sharp が関数内で認識できず、不良関数として落ちていた。

src/functions/resize.ts
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 に忘れないように書いておく。

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 が指摘してくれると良いなぁ。こんなに頻繁に行われそうな内容なのに、ドンピシャなドキュメントも見当たらなかったので、備忘録として。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?