最近、ブログの最適化を終えて利用者が増えたところ、Vercelから案内メールが届くようになりました。
内容は次のとおりです。
Your site is growing!
Your free team promes-projects has used 75% of the included free tier usage for Image Optimization - Cache Writes (100,000 Writes).
サイトが成長しています。
無料チームpromes-projectsは、画像最適化のキャッシュ書き込みについて、無料枠100,000回の75%を使用しました。
Next.jsのImageタグには、画像を自動最適化して提供する機能があります。
CDNを通じて提供され、ユーザーが画像を要求すると最適化された画像を生成します。
ただし、この過程でキャッシュが蓄積し、無料枠を超えると追加費用がかかる可能性があります。
CDNを使って画像を最適化する
私はすでにCDNのキャッシュサーバーから画像を提供しているため、Vercelの画像最適化を使わなくても十分な速度を出せます。
そこで、Vercelの
画像最適化を無効にし
、AWS S3とLambdaで自動圧縮する仕組みを加えれば、従来と同じ速度で配信できると考えました。
AWSの公式ドキュメントを参考に、LambdaとS3でアップロード画像を自動リサイズする方法を整理しました。
まず二つのS3バケットを作ります。
一つは原画像のアップロード用、もう一つは最適化後の画像を保存するためのものです。
🖐️
リージョンを慎重に設定してください。ソウルはap-northeast-2です。
例:
original-image-bucket-prome
thumbnail-image-bucket-prome
バケットはAWSコンソールから簡単に作成できます。
名前は一意である必要があります。
設定はすべて既定値とし、アクセスは非公開にします。
作成後、二つのバケットがあることを確認します。
LambdaからS3とCloudWatch Logsへアクセスできるよう、読み書きの権限を設定します。
二つのS3バケットへの読み書きを許可するIAMポリシーを作ります。
AWSコンソールでIAM → ポリシー → ポリシーの作成へ進み、次の設定を使います。
IAM:ポリシー↗
{
"Version" : "2012-10-17" ,
"Statement" : [
{
"Effect" : "Allow" ,
"Action" : [
"logs:PutLogEvents" ,
"logs:CreateLogGroup" ,
"logs:CreateLogStream"
],
"Resource" : "arn:aws:logs:*:*:*"
},
{
"Effect" : "Allow" ,
"Action" : [
"s3:GetObject"
],
"Resource" : "arn:aws:s3:::*/*"
},
{
"Effect" : "Allow" ,
"Action" : [
"s3:PutObject"
],
"Resource" : "arn:aws:s3:::*/*"
}
]
}
次へ進み、名前を
LambdaS3Policyとして作成します。
このポリシーで、Lambda関数からS3へのアクセスを許可します。
IAM → ロール → ロールの作成へ進み、Lambdaを選んで先ほどのポリシーを関連付けます。
IAM:ロール↗
次へ進み、権限の追加でLambdaS3Policyを選び、名前をLambdaS3Roleとして作成します。
次はLambda関数を作ります。
AWS Lambdaはサーバーレスのコンピューティングサービスで、実行サーバーを管理することなくコードを書いてデプロイできます。
Lambdaコンソールで関数の作成をクリックし、次の設定で新しい関数を作ります。
特に実行ロールは、先ほど作成した
LambdaS3Roleを選びます。
🖐️
リージョンは必ずS3と同じにしてください。ソウルならap-northeast-2です。
小さなコードエディターが開きます。
まずトリガーを追加します。
S3を選び、イベントタイプをPUTに設定します。
これで、S3へ画像をアップロードするたびにLambda関数が実行されます。
次に自分のコンピューターへ移り、ローカルでコードを作成します。
Lambdaコンソールでも書けますが、ローカルのほうが作業しやすいためです。
Node.jsを使って関数を作ります。
未導入なら、
Node.js公式サイト↗ からインストールしてください。
mkdir resize-image
cd resize-image
npm init
resize-imageへ移動し、画像リサイズ用の
sharpをインストールします。
sharpは画像のリサイズに使うライブラリです。
npm install --platform=linux --arch=x64 sharp@0.32.6
続いて、フォルダー内に
index.mjsを作ります。
最終的なフォルダー構造は次のとおりです。
📦resize-image
┣ 📂node_modules
┣ 📜index.mjs
┣ 📜package-lock.json
┗ 📜package.json
このファイルがLambda関数のエントリーポイントです。
次のコードを貼り付けます。
S3へアップロードされた画像を取得してリサイズし、別のS3バケットへ保存するコードです。
AWSリサイズのサンプルコード // dependencies
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3' ;
import { Readable } from 'stream' ;
import sharp from 'sharp' ;
import util from 'util' ;
// create S3 client
const s3 = new S3Client ({region: 'us-east-1' });
// define the handler function
export const handler = async ( event , context ) => {
// Read options from the event parameter and get the source bucket
console. log ( "Reading options from event: \n " , util. inspect (event, {depth: 5 }));
const srcBucket = event.Records[ 0 ].s3.bucket.name;
// Object key may have spaces or unicode non-ASCII characters
const srcKey = decodeURIComponent (event.Records[ 0 ].s3.object.key. replace ( / \+ / g , " " ));
const dstBucket = srcBucket + "-resized" ;
const dstKey = "resized-" + srcKey;
// Infer the image type from the file suffix
const typeMatch = srcKey. match ( / \. ( [ ^ .] * ) $ / );
if ( ! typeMatch) {
console. log ( "Could not determine the image type." );
return ;
}
// Check that the image type is supported
const imageType = typeMatch[ 1 ]. toLowerCase ();
if (imageType != "jpg" && imageType != "png" ) {
console. log ( `Unsupported image type: ${ imageType }` );
return ;
}
// Get the image from the source bucket. GetObjectCommand returns a stream.
try {
const params = {
Bucket: srcBucket,
Key: srcKey
};
var response = await s3. send ( new GetObjectCommand (params));
var stream = response.Body;
// Convert stream to buffer to pass to sharp resize function.
if (stream instanceof Readable ) {
var content_buffer = Buffer. concat ( await stream. toArray ());
} else {
throw new Error ( 'Unknown object stream type' );
}
} catch (error) {
console. log (error);
return ;
}
// set thumbnail width. Resize will set the height automatically to maintain aspect ratio.
const width = 200 ;
// Use the sharp module to resize the image and save in a buffer.
try {
var output_buffer = await sharp (content_buffer). resize (width). toBuffer ();
} catch (error) {
console. log (error);
return ;
}
// Upload the thumbnail image to the destination bucket
try {
const destparams = {
Bucket: dstBucket,
Key: dstKey,
Body: output_buffer,
ContentType: "image"
};
const putResult = await s3. send ( new PutObjectCommand (destparams));
} catch (error) {
console. log (error);
return ;
}
console. log ( 'Successfully resized ' + srcBucket + '/' + srcKey +
' and uploaded to ' + dstBucket + '/' + dstKey);
};
私はリサイズとWebP変換の両方が必要だったため、次のコードを使いました。
regionとbucketの名前
は、自分の環境に合わせて変更してください。
original-image-bucket-promeへアップロードした画像をリサイズし、
resized-image-bucket-promeへ保存します。
元の画像形式に応じて、変換後の拡張子を変更します。
たとえば
image.jpgは、リサイズ後に
image.webpとして保存します。
元からWebPの場合は、リサイズのみ行います。
GIFのようなアニメーション画像にも対応しています。
AWSリサイズ・WebP変換のサンプルコード // dependencies
import {
S3Client,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3" ;
import { Readable } from "stream" ;
import sharp from "sharp" ;
// create S3 client
const s3 = new S3Client ({ region: "ap-northeast-2" });
// define the handler function
export const handler = async ( event , context ) => {
const srcBucket = event.Records[ 0 ].s3.bucket.name;
const srcKey = decodeURIComponent (
event.Records[ 0 ].s3.object.key. replace ( / \+ / g , " " )
);
const dstBucket = "resized-image-bucket-prome" ; // 리사이즈된 이미지를 저장할 버킷 이름
const filename = srcKey. split ( "/" ). pop (); // 파일명만 추출
const dstKey = `${ filename . replace ( / \. [ ^ .] +$ / , ".webp" ) }` ;
const typeMatch = srcKey. match ( / \. ( [ ^ .] * ) $ / );
if ( ! typeMatch) {
return ;
}
const imageType = typeMatch[ 1 ]. toLowerCase ();
// WebP 파일은 변환 없이 그대로 복사
if (imageType === "webp" ) {
try {
const params = {
Bucket: srcBucket,
Key: srcKey,
};
var response = await s3. send ( new GetObjectCommand (params));
var stream = response.Body;
if (stream instanceof Readable ) {
var content_buffer = Buffer. concat ( await stream. toArray ());
} else {
throw new Error ( "알 수 없는 객체 스트림 타입입니다" );
}
const destparams = {
Bucket: dstBucket,
Key: dstKey,
Body: content_buffer,
ContentType: "image/webp" ,
};
await s3. send ( new PutObjectCommand (destparams));
return ;
} catch (error) {
return ;
}
}
try {
const params = {
Bucket: srcBucket,
Key: srcKey,
};
var response = await s3. send ( new GetObjectCommand (params));
var stream = response.Body;
if (stream instanceof Readable ) {
var content_buffer = Buffer. concat ( await stream. toArray ());
} else {
throw new Error ( "알 수 없는 객체 스트림 타입입니다" );
}
} catch (error) {
return ;
}
const width = 600 ;
const height = 600 ;
try {
let sharpInstance = sharp (content_buffer, {
animated: true , // 애니메이션 GIF 지원
pages: - 1 , // 모든 프레임 처리
}). resize (width, height, {
fit: sharp.fit.inside,
});
var output_buffer = await sharpInstance
. webp ({
lossless: false ,
animated: true , // 애니메이션 보존
})
. toBuffer ();
} catch (error) {
return ;
}
try {
const destparams = {
Bucket: dstBucket,
Key: dstKey,
Body: output_buffer,
ContentType: "image/webp" ,
};
await s3. send ( new PutObjectCommand (destparams));
} catch (error) {
return ;
}
};
resize-imageの中身をZIPファイルにまとめます。
index.mjsと
node_modulesを含めて圧縮します。
特に
node_modulesは必ず含めてください。
AWS LambdaではNode.jsランタイムを使うため、必要な依存関係をすべて同梱する必要があります。
作成したZIPをLambdaコンソールへ戻ってアップロードします。
コードタブのアップロードからZIPを選択します。
アップロードが完了すると、Lambda関数が作成されます。
S3へ画像をアップロードすると、Lambdaが自動実行され、リサイズした画像がもう一方のバケットへ保存されます。
テスト用に
original-image-bucket-promeへ画像をアップロードします。
うまく動かなければ、テストタブでイベントを直接作成できます。
まず
original-image-bucket-promeへ画像をアップロードし、テストタブでイベントを作ります。
次のようにS3 PUTテンプレートを作成します。
次のJSONイベントの4か所を、自分の環境に合わせて修正します。
{
"Records" : [
{
"eventVersion" : "2.0" ,
"eventSource" : "aws:s3" ,
"awsRegion" : "ap-northeast-2" , // 리전
"eventTime" : "1970-01-01T00:00:00.000Z" ,
"eventName" : "ObjectCreated:Put" ,
"userIdentity" : {
"principalId" : "EXAMPLE"
},
"requestParameters" : {
"sourceIPAddress" : "127.0.0.1"
},
"responseElements" : {
"x-amz-request-id" : "EXAMPLE123456789" ,
"x-amz-id-2" : "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"
},
"s3" : {
"s3SchemaVersion" : "1.0" ,
"configurationId" : "testConfigRule" ,
"bucket" : {
"name" : "original-image-bucket-prome" , // 사진이 저장된된 버킷 이름
"ownerIdentity" : {
"principalId" : "EXAMPLE"
},
"arn" : "arn:aws:s3:::original-image-bucket-prome" // 사진이 저장된된 버킷 ARN
},
"object" : {
"key" : "1234.png" , // 파일 이름
"size" : 1024 ,
"eTag" : "0123456789abcdef0123456789abcdef" ,
"sequencer" : "0A1B2C3D4E5F678901"
}
}
}
]
}
テストをクリックすると、成功または失敗のメッセージが表示されます。
成功したら、
resized-image-bucket-promeに画像が保存されていることを確認します。
失敗したら、CloudWatch Logsでエラーメッセージを確認します。
Vercelの画像最適化を無効にし、AWS LambdaとS3でアップロード時にリサイズとWebP変換を自動実行する方法を紹介しました。
サーバーレス環境で画像処理を自動化するのに役立ちます。
Lambdaならサーバーを管理せずにコードを実行でき、S3で画像を保存・管理できます。
これにより、ブログやウェブサイトの性能を改善できます。
リサイズ前
リサイズ後
これでVercelの画像最適化とはお別れです。✋
module . exports = {
images: {
unoptimized: true ,
},
};