初めに
こんにちは、株式会社BFT名古屋支店新人エンジニアのないとうです。
先日、API Gatewayで発生した502エラーについての原因の調査と、対応を行いました。
今回はその時の行動について書きたいと思います。
システムについて
まず、エラーの起こったシステムについて関連する部分を紹介します。
画像のアップロード機能
画像がS3に保存されたことをトリガーに【EXIFデータを取得するLambda】を起動します。
そしてLambdaによって画像の情報をDBに保存し、画像は1/5のサイズにリサイズし別バケットに保存します。
(EXIFデータを取得するLambdaについて詳しくはこちら→【Lambda】【python】画像からEXIFデータを取得するLambdaを作成する - BFT名古屋 TECH BLOG)
画像の取得機能
APIを呼び出すことで、【画像を取得するLambda】が起動します。
そして画像をbase64に変換してAPI Gatewayに送信することでAPIがbase64形式の画像を出力します。
(画像の取得機能について詳しくはこちら→ 【API Gateway】API Gatewayを用いてLambda無しでS3に画像をアップロードする方法 - BFT名古屋 TECH BLOG)
エラーについて
エラーが起こったのは画像の取得機能です。
通常APIを呼び出すと画像が出力されますが、APIを呼び出したところ502エラーが出力されました。
原因の調査①
502エラーについてゲートウェイレスポンスを確認したところ、該当するエラーコードは存在しませんでした。
調査したところLambda上でエラーが発生する可能性があると記載がありました。
そこで【画像を取得するLambda】のテストを行ったところレスポンスが6MBを超えLambdaが異常終了していました。
エラー対応
エラーを解消するためにはLambdaのレスポンスが6MBを超えないようにする必要があります。
しかし【EXIFデータを取得するLambda】では画像を1/5のサイズにリサイズしているため、Lambda上でのリサイズによってbase64のサイズが変わらないと考えました。
署名付きURLを発行する
画像データをAPIに送信する方法は、base64に変換する方法の他に一時的に画像を表示することができる署名付きURLを発行する方法があります。
手順としては【画像を取得するLambda】のプログラムを以下の内容に変更します。
import boto3 def get_img_from_s3(FILE_PATH): BUCKET = 'YOUR_BUCKET' KEY = 'FILE_PATH' body=s3.generate_presigned_url( ClientMethod = 'get_object', Params = {'Bucket' : BUCKET, 'Key' : KEY}, ExpiresIn = 3600, HttpMethod = 'GET') return body def lambda_handler(event, context): name = event['queryStringParameters']['name'] img = get_img_from_s3(name) return { 'headers': { "Content-Type": "image/jpeg" }, 'statusCode': 200, 'body': img }
実装して確認したところ、画像は表示されずダウンロードされました。
そのためエラー解決には至りませんでした。
JSで画像をリサイズする
次に画像をS3に保存するまえにJavaScriptでリサイズを行うことにしました。
リサイズを行う方法として 次の動作を行う関数を用いました。
1.画像のサイズよりもサイズを小さくしたcanvas上に画像を描画する
2.画像をcanvasに描写することでサイズを変更する
3.canvasの画像を取得する
具体的には以下のようなプログラムです。
onImageChange(e) { this.smallImages = []; const files = e.target.files; for(let file of files) { let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (e) => { let img = new Image(); img.onload = () => { let width = img.width; let height = img.height; //canvasサイズ設定 if(width > this.maxWidth) { height = Math.round(height * this.maxWidth / width); width = this.maxWidth; } //canvasの設定 let canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; let ctx = canvas.getContext('2d'); //画像描画 ctx.drawImage(img, 0, 0, width, height); //画像取得 ctx.canvas.toBlob((blob) => { const imageFile = new File([blob], file.name, { type: file.type, lastModified: Date.now() }); this.smallImages.push(imageFile); }, file.type, 1); }; img.src = e.target.result; }; } }
この方法で画像のリサイズが行えたのですがEXIFデータが削除されてしまいました。
今回のシステムではEXIFデータをDBに保存するためこの方法は取れませんでした。
Lambdaで画像をさらに小さくする
私はLambdaによってリサイズされた場合はbase64のサイズが変わらないと考えたのですが、それを確かめるために画像サイズを1/5から1/10にして確認しました。
リサイズ方法として、冒頭でリンクを載せた【EXIFデータを取得するLambda】の一部を紹介します。
import json import urllib.parse import boto3 s3 = boto3.client('s3') def lambda_handler(event, context): bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus( event['Records'][0]['s3']['object']['key'], encoding='utf-8') try: //画像取得 s3.download_file(bucket, key, "/tmp/img.jpg") //画像をリサイズ img=img.resize((w//10,h//10)) img.save("/tmp/upload.jpg") //リサイズした画像を保存 s3.upload_file("/tmp/upload.jpg","【保存先のバケット】",key) return 0 except Exception as e: print(e) print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket)) raise e
リサイズした画像を表示してみたところ、502エラーが解消されました。
これにより、私の考えが間違っていることがわかりました。
ではなぜ1/5のリサイズではエラーが発生し、1/10ではエラーが発生しなかったのでしょうか。
原因調査②
base64変換時の画像のサイズについて
ますbase64に画像が変換された場合どの程度のサイズまで表示可能であるかを調査しました。
base64は1バイトを6ビットで表現するため、base64変換を行うとサイズが約4/3倍になります。
具体的には3MBのサイズを持つ画像をbase64に変換すると約4MBになるということです。
すなわちLambdaの最大レスポンスである6MBで表現できる画像は、約4.5MBの画像までとなります。
リサイズを行っていない画像は、3~6MBほどのサイズだったので種類によってはエラーが発生する可能性があります。
しかし、1/5にリサイズした画像は100KB前後のサイズまで小さくなるので、エラーは発生しません。
試しに1/5にリサイズするようにLambdaを変更したところ、エラーは発生しませんでした。
Lambdaのレスポンスが6MBを超えた原因
base64のサイズの調査をしてわかったことは、
画像を表示するLambdaがリサイズした画像ではなくリサイズする前の、画像を参照したために発生したということです。
【EXIFデータを取得するLambda】では別バケットにリサイズした画像を保存するようなプログラムとなっています。
しかし【S3の画像を取得するLambda】でリサイズ前のバケットを参照していたことでエラーが発生していました。
終わりに
今回のエラーは終わってみれば単純なミスでした。
しかし自分の考えを検証しなかったことで、多くの時間を使ってしまいました。
これからは「考えたことは検証すること」、「単純なミスがないか確認すること」意識したいと思います。
ただ、エラーの調査や対応について勉強になったので良い経験だったと思います。
それでは。
参照