HEROPY
Tech
AWS [email protected]로 실시간 이미지 리사이징(수정됨-190826)
{{ scrollPercentage }}%

AWS [email protected]로 실시간 이미지 리사이징(수정됨-190826)

aws[email protected]cloudfronts3cloudwatchcloud 9

변경사항

2019년 8월

  • [email protected]에 대한 제한사항을 추가했습니다.
  • response.body가 1MB 이상일 경우에 대한 예외처리 코드를 추가했습니다.
  • Sharp 라이브러리의 GIF 변환 이슈와 관련해 예외처리 방법을 추가했습니다.
  • 좀 더 간편한 Lambda 새 버전 게시 방법을 추가했습니다.
  • CloudFront Cache에서 객체(파일)를 무효화하는 방법을 추가했습니다.

AWS [email protected]로 실시간 이미지 리사이징

[email protected]는 Amazon CloudFront의 기능 중 하나로서, 서버를 프로비저닝하고, 코드(Node.js)를 AWS Lambda에 업로드하고 요청에 대한 응답으로 함수가 사용자에게 좀 더 가까운 위치(리전)에서 트리거 되도록 구성할 수 있습니다.

[email protected]를 이용해 다음과 같이 쿼리스트링을 옵션으로 요청에 따라 실시간으로 이미지의 크기(w, h), 품질(q), 파일 형식(포맷, f)을 변경할 수 있도록 구성하고자 합니다.

https://heropy.blog/heropy.png?w=150&q=70&f=webp

CloudCraft Lambda@Edge for Image Resizing

IAM 정책 및 역할 생성

정책 생성

람다(Lambda)를 CloudFront 배포(Deploy)와 연결하기 위한 IAM 권한을 설정합니다.

IAM 정책

  • s3:PutObject 권한은 필요치 않습니다.
  • cloudfront:CreateDistribution 또는 cloudfront:UpdateDistribution 중 하나만 설정합니다.
  • CloudWatch에서 로그 데이터를 처리하기 위해 logs:xxx 권한들을 설정합니다.

IAM / 정책

  1. 정책 생성을 선택합니다.
  2. JSON을 선택해 아래 정책을 입력합니다.
  3. 정책의 이름(ResizingImages)과 설명을 작성합니다.
  4. 정책 생성!
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "lambda:GetFunction",
                "lambda:EnableReplication",
                "cloudfront:UpdateDistribution",
                "s3:GetObject",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Resource": "*"
        }
    ]
}

역할 생성

IAM 역할

IAM / 역할

  • 역할 만들기를 선택해 다음과 같이 진행합니다.
    1. ‘이 역할을 사용할 서비스’로 Lambda를 선택합니다.
    2. 목록 중 위에서 생성한 정책 ResizingImages를 선택합니다.
    3. 역할의 이름(ResizingImages)과 설명을 작성합니다.
    4. 역할 만들기!

역할의 신뢰 관계(정책) 수정

서비스 보안 주체 lambda.amazonaws.comedgelambda.amazonaws.com에 권한을 위임하기 위해 IAM 역할을 수정합니다.

  1. 나의 역할 목록에서 방금 생성한 ResizingImages 선택합니다.
  2. 신뢰 관계 탭의 신뢰 관계 편집 선택을 선택합니다.
  3. 아래의 신뢰 관계 정보를 입력합니다.
  4. 신뢰 정책 업데이트!
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

S3 설정 확인

  • 버킷(Bucket)의 리전(Region)은 상관없습니다.
  • 추후 CloudFront 설정 후 버킷 ‘권한/버킷정책’의 CloudFront Origin Access Identity가 연결한 CloudFront의 Origin Access Identity인지 확인하시면 됩니다.
  • 테스트를 위한 버킷의 퍼블릭 액세스 권한이 필요할 수 있습니다.
  • 버킷을 소유한 AWS 계정이 객체도 소유해야 합니다.
  • 요청된 객체가 버킷에 존재해야 합니다.
  • 버킷 Root 경로에 favicon.ico 파일을 업로드하세요. S3 Favicon error가 발생할 수 있습니다.

S3 Favicon error

Lambda 함수 생성

다음 [email protected]에 대한 제한을 주의합니다.

엔터티오리진 요청 및
응답 이벤트 제한
최종 사용자 요청 및
응답 이벤트 제한
함수 리소스 할당Lambda 제한과 동일128MB
함수 제한 시간30초5초
헤더 및 본문을 포함하여 Lambda 함수에서 생성되는 응답의 크기1MB40KB
Lambda 함수 및 포함된 라이브러리의 최대 압축 크기50MB1MB
  1. [email protected]의 리전은 미국 동부(버지니아 북부)(us-east-1)만 허용됩니다.
  2. 함수 생성을 선택해 다음과 같이 설정합니다.
    1. 함수 이름(ResizingImages)을 입력합니다.
    2. 런타임(Node.js 10.x)을 선택합니다.
    3. 실행 역할 선택 또는 생성
      • 실행 역할: 기존 역할 사용
      • 기존 역할: ResizingImages
  3. 함수 생성!
  4. ‘제한 시간’을 3초로 설정하면 최초 요청(Cache miss)에 연산이 많아지면 리사이징되지 않을 수 있습니다. 10초로 설정합니다.
  5. 저장!(화면 오른쪽 위)

리전을 미국 동부(버지니아 북부)이 아닌 아시아 태평양(서울)로 설정했더니 다음과 같이 에러 메시지가 두둥!

람다 리전 에러

Cloud 9을 이용한 Lambda 함수 작성

로컬(macOS)에서 람다 코드를 세팅하고 zip파일로 업로드하니,

"'darwin-x64' binaries cannot be used on the 'linux-x64' platform. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'."

라는 에러 메시지가 출력되네요.
node_modules 디렉터리에 있는 Sharp 바이너리는 linux-x64 플랫폼용으로 설정되어야 하기 때문에,
docker-lambda로 람다 런타임과 일치하는 환경을 복제해 사용할 수 있습니다.
하지만 저는 다른 방법으로 파일 업로드(zip)를 하지 않고 작성하고 싶어 Cloud 9을 사용했습니다.

모듈은 Sharp만 설치하시면 됩니다.

Cloud 9 Details

Cloud 9 / Your environments

람다 코드를 작성하기 위해 새로운 Cloud 9 환경을 설정합니다.

아직 서울 리전은 지원되지 않습니다.

  1. Create environment를 선택합니다.
  2. Name environment의 Name(ResizingImages)과 Description을 입력합니다.
  3. 다음과 같이 Configure settings를 설정했습니다.
    • Create a new instance for environment (EC2)
    • t2.micro (1 GiB RAM + 1 vCPU)
    • Amazon Linux
    • After 30 minutes (default)
  4. Create environment!

위에서 생성했던 람다 함수를 Cloud 9 환경으로 불러옵니다.

  1. 화면 오른쪽 위 메뉴 중 AWS Resources를 선택합니다.
  2. Lambda(us-east-1)/Remote Functions 목록의 ResizingImagesImport합니다.
  3. 불러온 람다 함수로 접근하기 위해 터미널(Terminal)을 이용합니다.(package.json을 생성하고 Sharp 모듈을 설치합니다.)
    1. $ cd ResizingImages
    2. $ npm init -y
    3. $ npm i sharp
  4. index.js을 아래 코드와 같이 수정합니다.
  5. 작성한 람다 함수를 $LATEST 버전으로 배포합니다.
    • Lambda(us-east-1)/Local Functions 목록의 ResizingImagesDeploy합니다.
'use strict';

const querystring = require('querystring'); // Don't install.
const AWS = require('aws-sdk'); // Don't install.
const Sharp = require('sharp');

const S3 = new AWS.S3({
  region: '<YOUR_BUCKET_REGION>'
});
const BUCKET = '<YOUR_BUCKET_NAME>';

exports.handler = async (event, context, callback) => {
  const { request, response } = event.Records[0].cf;
  // Parameters are w, h, f, q and indicate width, height, format and quality.
  const params = querystring.parse(request.querystring);

  // Required width or height value.
  if (!params.w && !params.h) {
    return callback(null, response);
  }

  // Extract name and format.
  const { uri } = request;
  const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);

  // Init variables
  let width;
  let height;
  let format;
  let quality; // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
  let s3Object;
  let resizedImage;

  // Init sizes.
  width = parseInt(params.w, 10) ? parseInt(params.w, 10) : null;
  height = parseInt(params.h, 10) ? parseInt(params.h, 10) : null;

  // Init quality.
  if (parseInt(params.q, 10)) {
    quality = parseInt(params.q, 10);
  }

  // Init format.
  format = params.f ? params.f : extension;
  format = format === 'jpg' ? 'jpeg' : format;

  // For AWS CloudWatch.
  console.log(`parmas: ${JSON.stringify(params)}`); // Cannot convert object to primitive value.
  console.log(`name: ${imageName}.${extension}`); // Favicon error, if name is `favicon.ico`.

  try {
    s3Object = await S3.getObject({
      Bucket: BUCKET,
      Key: decodeURI(imageName + '.' + extension)
    }).promise();
  } catch (error) {
    console.log('S3.getObject: ', error);
    return callback(error);
  }

  try {
    resizedImage = await Sharp(s3Object.Body)
      .resize(width, height)
      .toFormat(format, {
        quality
      })
      .toBuffer();
  } catch (error) {
    console.log('Sharp: ', error);
    return callback(error);
  }

  const resizedImageByteLength = Buffer.byteLength(resizedImage, 'base64');
  console.log('byteLength: ', resizedImageByteLength);

  // `response.body`가 변경된 경우 1MB까지만 허용됩니다.
  if (resizedImageByteLength >= 1 * 1024 * 1024) {
    return callback(null, response);
  }

  response.status = 200;
  response.body = resizedImage.toString('base64');
  response.bodyEncoding = 'base64';
  response.headers['content-type'] = [
    {
      key: 'Content-Type',
      value: `image/${format}`
    }
  ];
  return callback(null, response);
};

만약 GIF 포맷을 사용하는 경우 원본 그대로 반환하도록 예외처리할 수 있습니다.

Sharp 라이브러리에서 GIF 포맷을 다시 GIF로 리사이징하는데 문제를 확인했습니다.
관련 이슈는 https://github.com/lovell/sharp/issues/1372에서 확인할 수 있습니다.

// Exception '.gif' image.
if (extension === 'gif') {
  console.log('GIF image requested!');
  return callback(null, response);
}

다음과 같이 변환할 포맷이 없는 경우만 처리할 수도 있습니다.

if (extension === 'gif' && !params.f) {
  console.log('GIF image requested!');
  return callback(null, response);
}

혹은 CloudFront에서 예외처리할 수도 있습니다.

캐시 동작 순서를 주의합니다!

  1. CloudFront로 이동해 해당 Distribution을 선택합니다.
  2. Behaviors 탭에서 Create Behavior를 선택합니다.
  3. 다음과 같이 수정 후 Create!
    • Path Pattern: *.gif
    • Compress Objects Automatically: Yes

CloudFront Create Behavior

Lambda 새 버전 게시

$LATEST 버전으론 [email protected]를 사용할 수 없음으로 새로운 버전을 게시하여 CloudFront에 연결합니다.
내용을 수정했다면 수정된 버전을 게시하여 CloudFront에 연결해야 합니다.

Lambda 새 버전 게시

Lambda / 함수

  1. 함수 목록에서 ResizingImages를 선택합니다.
  2. 오른쪽 위 메뉴 중 ‘작업’의 새 버전 게시를 선택합니다.
  3. ‘$LATEST의 새 버전 게시’의 ‘버전 설명’을 입력하고 새로운 버전을 게시합니다.
  4. 게시된 새 버전의 ARN 복사(ARN - arn:aws:lambda~~~:ResizingImages:1, 화면 오른쪽 위)하여 CloudFront에 연결해야 합니다.

만약 람다를 수정했다면 다음과 같이 새 버전을 게시하고 CloudFront와 연결합니다.

  1. 게시된 새 버전의 ARN 복사(ARN - arn:aws:lambda~~~:ResizingImages:2, 화면 오른쪽 위)합니다.
  2. CloudFront로 이동해 해당 Distribution을 선택합니다.
  3. Behaviors 탭에서 기본 Behavior를 체크하고 Edit를 선택합니다.
  4. 다음과 같이 수정 후 Yes, Edit!
    • Lambda Function Associations:
      • Lambda Function ARN: Lambda 함수에서 복사한 ARN 입력(arn:aws:lambda~~~:ResizingImages:2)

만약 람다를 수정했다면 좀 더 쉽게 새 버전을 게시하고 CloudFront와 연결할 수 있습니다.

CloudFront 설정이 미리 필요합니다.

  1. 수정한 $LATEST 버전에서 ‘작업’의 [email protected] 배포를 선택합니다.
  2. '이 함수에 기존 CloudFront 트리거 사용을 선택하고 내용을 확인한 뒤 배포합니다.

기존 트리거 사용

새로운 트리거를 구성할 경우 다음과 같이 설정할 수 있습니다.

새로운 트리거 구성

CloudFront

CloudFront / Distributions

  1. Create Distribution을 선택합니다.
  2. Web에서 Get Started를 선택하고 다음과 같이 설정합니다.
    • Origin Domain Name: 웹 콘텐츠를 가져올 AWS S3 Bucket
    • Restrict Bucket Access: Yes(항상 CloudFront URL로 S3 액세스하도록 요구)
    • Origin Access Identity: Create a New Identity
    • Grant Read Permissions on Bucket: Yes, Update Bucket Policy(CloudFront가 S3의 버킷 정책에 액세스하여 업데이트)
    • Query String Forwarding and Caching: Forward all, cache based on whitelist
    • Query String Whitelist: w, h, f, q(CloudFront가 사용할(캐싱 기반) 쿼리스트링 매개 변수 정의)
    • Compress Objects Automatically: Yes(Accept-Encoding: gzip 요청에 대한 콘텐츠 압축 여부)
    • Lambda Function Associations:
      • CloudFront Event: Origin Response
      • Lambda Function ARN: Lambda 함수에서 복사한 ARN 입력(arn:aws:lambda~~~:ResizingImages:1)
    • Comment: ResizingImages(Distribution 구분을 위한 간략한 이름/설명)
  3. Create Distribution!

Distribution의 ‘Status’를 확인하세요. In Progress에서 Deployed로 변경되는데 10~15분 정도 소요됩니다.

CloudFront 배포 중

CloudFront 설정에서 대체 도메인 이름(CNAME)을 사용하면 CloudFront에서 배포용 도메인 이름 대신에 고유의 도메인 이름(예: www.heropy.blog)이 사용됩니다.
이 포스트에선 대체 도메인을 지정하지 않습니다.

CloudFront CNAME

확인

CloudFront Distribution가 배포 완료되면, CloudFront Domain Name으로 다음 예제와 같이 이미지에 접근할 수 있습니다.

d2d73zr4p2zskr.cloudfront.net/heropy.png?w=120&f=webp&q=90

쿼리스트링의 순서가 바뀌지 않도록 주의합니다!

CloudWatch 로그 확인

CloudWatch를 통해 람다 함수에서 발생하는 로그를 확인할 수 있습니다.
로그를 확인할 수 없다면 ‘서울’ 리전을 확인해 보세요.

CloudWatch Logs

Cache miss vs Cache hit

한 번의 테스트에서 PNG 포맷의 원본 이미지(500x500px)를 555px 크기의 WEBP 포맷으로 변경하는데,
‘Cache miss’ 경우 5.26s가, ‘Cache hit’ 경우 31ms가 소요되었습니다.

cache miss vs cache hit

CloudFront 엣지 캐시에서 파일 무효화

CloudFront 엣지 캐시에서 파일이 만료되기 전에 파일을 제거해야 할 경우, 다음과 같이 설정합니다.

  1. CloudFront로 이동해 해당 Distribution을 선택합니다.
  2. Invalidations 탭에서 Create Invalidation을 선택합니다.
  3. Object Paths에 경로를 입력합니다.
    • E.g. /heropy.png, /*, /images/*
  4. Invalidate!

Invalidate file

참고자료

https://engineering.huiseoul.com/lambda-%ED%95%9C%EA%B0%9C%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-on-demand-image-resizing-d48167cc1c31
https://ikso2000.tistory.com/106
https://medium.com/daangn/aws-lambda%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EC%83%9D%EC%84%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-acc278d49980
https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html
https://jasonbla.tistory.com/5
https://github.com/lovell/sharp/issues/1372
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-lambda-at-edge

공지 이미지
{{ title }}
{{ eventPlace }}
{{ eventDate }} ({{ eventDay }}) {{ eventTime }}
오늘 하루 그만 보기