[AWS] Lambda@Edge로 이미지 webp 변환

1. 개요

Lambda@Edge 는 전 세계에 위치하는 AWS Edge Location에 배포되어 동작하는 Lambda 함수이다.

버지니아 리전에 생성된 Lambda가 Edge Location으로 복제되는 방식이다.

이 글에서는 이미지를 webp 확장자로 변환하여 서비스하는 방법을 소개한다.

image

사용자가 CloudFront를 통해 이미지 파일을 요청하면, CloudFront는 Origin S3에게 해당 이미지를 요청한다.

Lambda 함수는 S3의 응답을 받아 webp로 변환 후 사용자에게 응답한다.

변환된 이미지는 CloudFront에 캐시로 남아 동일한 요청이 오면 Lambda 함수가 동작하지 않고 캐시로 응답한다.

2. 구성

1) S3 버킷 생성

프라이빗 S3 버킷을 생성한다.

image

테스트 jpg 이미지를 업로드해놓는다.

image

2) CloudFront 생성

이 S3를 원본으로 하는 CloudFront 배포를 생성한다. 프라이빗 S3에 접근하기 위해 OAI를 설정한다.

image

CloudFront로 요청하면, 지금은 jpg확장자로 응답된다.

image

3) Lambda 권한 설정

Lambda가 사용할 IAM 역할을 생성한다.

정책은 아래와 같다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:CreateServiceLinkedRole",
        "lambda:GetFunction",
        "lambda:EnableReplication",
        "cloudfront:UpdateDistribution",
        "s3:GetObject",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": "*"
    }
  ]
}

lambda 뿐 아니라 lambda@edge도 사용할 수 있게 신뢰관계를 아래와 같이 편집한다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": ["edgelambda.amazonaws.com", "lambda.amazonaws.com"]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

이 역할을 사용하는 Node.js 14 Lambda 를 생성한다.

image

Lambda 함수가 프라이빗 S3 버킷에 접근할 수 있어야 한다.

위에서 생성한 Lambda 역할에 대해 접근을 허용하도록 버킷 정책을 편집한다.

{
...
        {
            "Sid": "GetObject",
            "Effect": "Allow",
            "Principal": {
                "AWS": "<Lambda Role ARN>"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<버킷명>/*"
        },
        {
            "Sid": "ListBucket",
            "Effect": "Allow",
            "Principal": {
                "AWS": "<Lambda Role ARN>"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::<버킷명>"
        }
...
}

4) Lambda 함수 작성

이제 Lambda 함수를 작성해본다.

npm 패키지를 설치해야 하는데, 이 작업은 로컬에서 해도 무방하다.

이 글에서는 Cloud9 환경에서 진행한다.

Lambda를 Cloud9으로 다운로드한다.

image

아래의 함수를 사용한다.

"use strict";

const querystring = require("querystring");
const AWS = require("aws-sdk");
const Sharp = require("sharp");

const S3 = new AWS.S3({
  signatureVersion: "v4",
  region: "ap-northeast-2", // 버킷을 생성한 리전 입력
});

// 버킷명
const BUCKET = "버킷명";

// 호환 가능한 확장자
const supportImageTypes = ["jpg", "jpeg", "png", "gif", "webp", "svg", "tiff"];

exports.handler = async (event, context, callback) => {
  const { request, response } = event.Records[0].cf;

  const { uri } = request;
  const ObjectKey = decodeURIComponent(uri).substring(1);

  const extension = uri.match(/\/?(.*)\.(.*)/)[2].toLowerCase();
  let s3Object;
  let resizedImage;

  // 호환 가능한 확장자가 아니면 원본 반환
  if (!supportImageTypes.some((type) => type === extension)) {
    return callback(null, response);
  }

  // Verify For AWS CloudWatch.
  console.log("S3 Object key:", ObjectKey);

  try {
    s3Object = await S3.getObject({
      Bucket: BUCKET,
      Key: ObjectKey,
    }).promise();

    console.log("S3 Object:", s3Object);
  } catch (error) {
    // 해당 이미지가 없을 경우의 응답
    responseHandler(404, "Not Found", "The image does not exist.", [
      { key: "Content-Type", value: "text/plain" },
    ]);
    return callback(null, response);
  }

  // sharp로 이미지 webp 변환
  try {
    resizedImage = await Sharp(s3Object.Body).webp().toBuffer();
  } catch (error) {
    responseHandler(500, "Internal Server Error", "Fail to resize image.", [
      {
        key: "Content-Type",
        value: "text/plain",
      },
    ]);
    return callback(null, response);
  }

  // 응답 이미지 용량이 1MB 이상일 경우 원본 반환.
  if (Buffer.byteLength(resizedImage, "base64") >= 1048576) {
    return callback(null, response);
  }

  responseHandler(
    200,
    "OK",
    resizedImage.toString("base64"),
    [
      {
        key: "Content-Type",
        value: `image/webp`,
      },
    ],
    "base64"
  );

  /**
   * @summary response 객체 수정을 위한 wrapping 함수
   */
  function responseHandler(
    status,
    statusDescription,
    body,
    contentHeader,
    bodyEncoding
  ) {
    response.status = status;
    response.statusDescription = statusDescription;
    response.body = body;
    response.headers["content-type"] = contentHeader;
    if (bodyEncoding) {
      response.bodyEncoding = bodyEncoding;
    }
  }

  console.log("Success resizing image");

  return callback(null, response);
};

sharp 패키지를 설치한다.

yuntreee:~/environment $ cd convert_webp/
yuntreee:~/environment/convert_webp $ npm init -y
yuntreee:~/environment/convert_webp $ npm install -y sharp

작성된 어플리케이션을 Lambda로 업로드한다.

image

image

image

image

5) Lambda@Edge 배포

업로드가 완료되면 이제 Lambda@Edge로 배포한다.

image

CloudFront event는 Origin Response 로 설정한다. 원본 (여기서는 S3 버킷)의 응답에 대해 Lambda가 동작한다.

image

배포한 CloudFront 동작->편집 을 확인해보면 Lambda@Edge가 배포되어있다.

image

image

동일한 이미지로 테스트하면 CloudFront에 남아있는 jpg 캐시가 반환되기 때문에 캐시를 삭제해준다.

image

image

3. 결과 확인

CloudFront로 jpg 파일을 요청하면 webp로 변환된 이미지를 확인할 수 있다.

첫 요청때는 Lambda가 동작하느라 Latency가 조금 있지만, 이후 요청은 CloudFront에 저장된 캐시로 빠르게 응답받는다.

image

카테고리:

업데이트:

댓글남기기