serverless 프레임워크를 통해 lambda를 배포하자

 

serverless 설치

npm i serverless -g

 

명령어로 시작

serverless

 

docs 에는 starter 로 시작하라고 하는데 최근 업데이트를 해서 선택지가 없다. (docs는 업데이트가 안됐다..)

Simple Function을 선택하자

 

AWS 자격 증명 설정

여기 중요하다. 처음에 Easy & Recommended로 만들었는데 한번 만들면 serverless 에 등록이 되어버려 삭제하고 다시 Iam을 등록해줘야된다. 최근 업데이트 된거라 docs에도 삭제관련 내용이 없어서 한참 해맸다.. 우린 이미 만들어준 iam 을 연결해야되니 Easy & Recommended 로 만들어주는 iam은 필요가없다.

2번째 Save AWS Credentials을 선택해준다.

만약 1번을 선택했다면 다음 과정을 따라해보자.

serverless login 후에 나오는 serverless app 에서 등록되어버린 iam을 직접 삭제해준다.

저 provider 부분은 lambda 배포할때 필요한거긴 한데 저게있으면 serverless 가 AWS 자격 증명 설정 선택지를 안준다..

그래서 저거 삭제하고 2번으로 선택한 후 저 provider iam을 만들어서 다시 넣어줬다

(업데이트 된지 얼마 안되서인지 docs에 관련 내용이 없어서 이부분은 계속 오류나면서 이것저것 해보다가 된거라 맞는 방법인지 확신은 없다)

 

플러그인 설치

npm install --save-dev serverless-lambda-edge-pre-existing-cloudfront

이미 있는 cloudfront에 lambda를 배포하려면 추가 작업이 필요하다. (관련 docs)

docs에 나온대로 yml 파일도 작성해주고

// serverless.yml

functions:
  hello:
    handler: index.hello // index 파일의 hello 함수 배포
    events:
      - preExistingCloudFront:
          # ---- Mandatory Properties -----
          distributionId: ID # CloudFront distribution ID you want to associate
          eventType: origin-response # Choose event to trigger your Lambda function, which are `viewer-request`, `origin-request`, `origin-response` or `viewer-response`
          pathPattern: "*" # Specifying the CloudFront behavior
          includeBody: false # Whether including body or not within request
          # ---- Optional Property -----
          # stage: dev # Specify the stage at which you want this CloudFront distribution to be updated
plugins:
  - serverless-lambda-edge-pre-existing-cloudfront

 

추가로 1탄에서 만든 iam 을 입력해준다

// serverless.yml

provider:
  name: aws
  runtime: nodejs20.x
  iam:
    role:
      arn:aws:iam::[YOUR IAM ARN]

 

 

이제 적당한 예시 함수를 배포해보자

// index.js

export const hello = async (event) => {
  console.log(JSON.stringify(event, null, 2));
  return event;
};

 

배포 명령어 입력

serverless depoly

 

배포가 성공하면 버지니아 북부 > Lambda > 함수 에서 확인할 수 있다. 

 

최신 버전으로 들어가면

 

트리거 Cloudfront가 잘 연결되어있다 

 

테스트 코드도 잘 들어가있고

 

잘 되는지 테스트도 할수있다.

querystring 으로 선택

 

아래 로그는 cloudwatch 로 연결되는데 로그를 더 정확하게 보고싶을때 사용한다.

(무수한 에러의 요청에 많이 보게된다..)

 

test 함수가 잘 배포되었으니 실제 리사이징 코드를 작성하자.

"use strict";
import Sharp from "sharp";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

// S3 클라이언트 초기화
const S3 = new S3Client({
  region: "ap-northeast-2",
});

// 쿼리스트링에서 특정 키의 값을 가져오는 함수
const getQuerystring = (querystring, key) => {
  return new URLSearchParams("?" + querystring).get(key);
};

// 이미지 리사이즈 함수
export const imageResize2 = async (event, context) => {
  console.log("imageResize 실행!!");

  // 이벤트에서 요청 데이터 가져오기
  const { request, response } = event.Records[0].cf;

  // 쿼리스트링 가져오기
  const querystring = request.querystring;
  
  // 쿼리스트링이 없으면 원본 이미지 반환
  if (!querystring) {
    console.log("querystring is empty!! return origin");
    return response;
  }

  // URI 디코딩
  const uri = decodeURIComponent(request.uri);

  // 파일 확장자 추출
  const extension = uri.match(/(.*)\.(.*)/)[2].toLowerCase();
  console.log("extension", extension);

  // GIF 파일은 리사이징하지 않고 원본 반환
  if (extension === "gif") {
    console.log("extension is gif!! return origin");
    return response;
  }

  // 쿼리스트링 파싱
  const width = Number(getQuerystring(querystring, "w")) || null;
  const height = Number(getQuerystring(querystring, "h")) || null;
  const fit = getQuerystring(querystring, "f");
  const quality = Number(getQuerystring(querystring, "q")) || null;
  console.log({ width, height, fit, quality });

  // S3 버킷 이름 및 경로 추출
  const s3BucketDomainName = request.origin.s3.domainName;
  let s3BucketName = s3BucketDomainName.replace(
    ".s3.ap-northeast-2.amazonaws.com",
    ""
  ).replace(".s3.amazonaws.com", "");
  console.log("s3BucketName", s3BucketName);
  
  const s3Path = uri.substring(1);
  console.log("s3Path: ", s3Path);

  // S3에서 이미지 가져오기
  let s3Object = null;
  try {
    s3Object = await S3.send(
      new GetObjectCommand({
        Bucket: s3BucketName,
        Key: s3Path,
      })
    );
    console.log("S3 GetObject Success");
  } catch (err) {
    console.log("S3 GetObject Fail!!", err);
    return err;
  }

  // 이미지 리사이즈 수행
  const s3Uint8ArrayData = await s3Object.Body.transformToByteArray();

  let resizedImage = null;
  try {
    resizedImage = await Sharp(s3Uint8ArrayData)
      .resize({ width, height, fit })
      .toFormat(extension, { quality })
      .toBuffer();
    console.log("Sharp Resize Success");
  } catch (err) {
    console.log("Sharp Resize Fail!!", err);
    return err;
  }

  // 리사이징한 이미지 크기 확인
  const resizedImageByteLength = Buffer.byteLength(resizedImage, "base64");
  console.log("resizedImageByteLength:", resizedImageByteLength);

  // 리사이징한 이미지가 1MB 이상일 경우 원본 반환
  if (resizedImageByteLength >= 1048576) {
    console.log("resizedImageByteLength >= 1048576!! return origin");
    return response;
  }

  // 리사이징한 이미지 응답 설정
  response.status = 200;
  response.body = resizedImage.toString("base64");
  response.bodyEncoding = "base64";
  console.log("imageResize 종료!!");
  return response;
};

(코드 출처)

 

실제 쿼리스트링에 따라 lambda 함수가 실행되는지 확인해보자

Miss from cloudfront 인 경우( cloudfront 캐싱 전일 때)

 

위의 lambda에 배포한 리사이징 함수가 실행된다.

 

새로고침 후 Hit fron cloudfront ( cloudfront 캐싱됐을때 )

 

lambda 함수가 실행되지 않는다

 

s3, cloudfront, lambda 등등 aws 인프라 이용하면서 DevOps에 흥미가 생겼고 단순히 기능을 구현하는 건 정말 개발의 일부구나 생각했다. (그리고 aws 주식을 사고 싶어졌다.)

다음은 요새 뜨고있는 cloudflare 인프라도 이용해보고 싶은데 도메인 연결해서 cloudflare 이미지 리사이징을 해보는것도 재밌겠다.

 

출처 및 참고했던 블로그

aws docs

serverless docs

 

그대로 따라하기 정말 좋았던 연우리님 블로그

inpa 블로그

inpa 블로그-2 (개념)

올리브영 테크블로그

'aws' 카테고리의 다른 글

AWS cloudfront & lambda@edge 로 이미지 리사이징 -1  (0) 2024.08.22

이번글은 회고보다는 이미지 리사이징하면서 만났던 무수한 에러들과 해결했던 부분을 정리하고자 한다.

(정리 안해놓으면 까먹을거 같아서..)

 

프로젝트 초기 구상 때부터 걸렸지만 외면하고 있던 부분이 있었다.

그것은 바로 매우매우 많은 이미지..

메인 페이지부터 이미지가 잔뜩인 나의 NFT 마켓플레이스..

Next.js 로 이미지 리사이징 하는것도 좋고 아님 node.js 서버 하나 돌려서 이미지 리사이징 처리해도 좋은데 

일단 스택을 React로 잡기도했고 이미지 리사이징 하려고 서버를 상시 돌린다는 것도 배보다 배꼽이 큰 느낌이라 다른 방법을 찾아봤다.

 

 Cloudflare vs AWS cloudfront & lambda@edge

serverless 컨셉을 유지하면서 이미지 리사이징 처리하는 방법으로 둘중에 고민했는데 Cloudflare가 요새 뜨기도 하고 사용법도 훨씬 간편한데 AWS 인프라를 사용해보고 싶어서 AWS로 선택했다.

Cloudflare의 이미지 관련 내용을 잠깐 설명하면 Next.js 이미지 리사이징과 AWS cloudfront(cdn)를 합친걸로 생각하면 되는데 아래 예시를 참고하면 쉽게 이해될듯하다.

//Next.js 이미지 리사이징

import Image from 'next/image';
<Image
    src="/path/to/your/image.jpg" // 이미지 경로
    width={500} // 원하는 너비
    height={300} // 원하는 높이
    layout="responsive" // 레이아웃 옵션
    quality={75} // 이미지 품질 (0-100)
/>
// cloudflare image 리사이징

// Docs: https://developers.cloudflare.com/images/url-format
export default function cloudflareLoader({ src, width, quality }) {
    const params = [`width=${width}`, `quality=${quality || 75}`, 'format=auto']
    return `https://example.com/cdn-cgi/image/${params.join(',')}/${src}`
}
요렇게 함수를 만들어서 호출하면 cloudflare 에 등록한 
url(https://example.com/ 도메인 등록 해줘야 쓸수 있음!)
을 통해 이미지를 리사이징 할 수 있다

위 cloudflareLoader를 호출해 url로 요청을 하면 cdn이 해당 내용을 캐싱해서 일정 시간동안은 리사이징을 진행하지 않고 캐싱해온 콘텐츠를 가지고와서 리소스 낭비가 없다.

 

처음엔 그냥 next.js 더 써보고 싶은데 Next.js 로 마이그레이션 해버릴까.. 를 생각했지만 이번 리사이징 진행하면서 좀더 알아보니 편하게만 생각했던 next/image 리사이징은 그만한 대가가 있었다.

 

사이드 프로젝트로 next/image 사용하던 중에 본인 노트북이 다운된적이 있다.

좀 오래 쓰긴했어도 그정도는 아닌데.. 같은 작업중에 몇번 더 다운되길래 디버깅해봤다.

image 리사이징이 많이 들어간 컴포넌트가 랜더링 되는 순간 다운되길래 CPU 사용량을 측정해보니 순간적으로 100%를 찍고 내려오고 있었다.

 

..잉?

이미지 프로세싱은 CPU 리소스를 많이 사용한다. 리사이징은 각 픽셀에 대한 연산을 수행하는데,

1920x1080 해상도의 이미지는 200만 픽셀이 넘는다. 모든 픽셀에 연산을 수행하고 다시 픽셀을 새로운 크기에 맞게 재배치 하는 과정이 CPU 부하를 준다. JPEG, Webp 확장자 처럼 용량을 압축한 포맷들도 압축을 해제 후 리사이징을 진행하기 때문에 원본 px값의 연산이 고스란히 진행된다.

 

만약 AWS 프리티어 계정의 서버에 올린다고 하면

AWS EC2 ts.micro의 1CPU, 1GB RAM 인스턴스 사양으로 이미지 리사이징이 몇개만 진행되도 CPU가 금방 100% 되는걸 예상할 수 있고(본인 노트북 사양 4CPU, 8GB RAM) RAM 사용량도 금방 100%가 넘어갈듯하다. 

AWS ts.micro 사양

 

이미지 프로세싱은 메모리에 원본 이미지를 로드하고 프로세싱 중의 buffer와 결과 값을 저장하는 등에 RAM 이 사용되는데 적당히 5MB PNG 파일로 계산해보자.

원본의 50%만 압축되었다고 가정해도 (2:1 ~ 5:1 압축률을 가진다)

압축해제 10MB( 5MB * 2  ) + 결과 값 10MB + (리사이징 중 Buffer 값) = 20MB 보다 크고

운영체제와 Next.js 서버 메모리 사용량을 제외하면  40개의 이미지 리사이징 만으로 RAM 사용량이 100%가 되버린다. 

 

(그리고 Next서운 이야기.. )

이미지 최적화의 캐싱 관련해서 가비지 컬렉터가 정상적으로 작동하지 않아 메모리가 100%가 되어버린다?

열려있는 메모리 누수 관련 Git issue

 

 

등등의 next/image의 그림자를 보고나니 lambda 함수로 로컬 서버 외의 리소스를 사용하는 접근이 맞다고 생각했다.

 

AWS cloudfront & lambda@edge (feat.s3)

 

AWS 인프라를 통해 이미지 리사이징을 처리하는 과정을 아래 아키텍처와 같다

0. (리사이징할) 이미지를 s3에 업로드 한다.

1. 사용자가 cloudfront url을 통해 s3 에 업로드한 이미지를 요청한다

2. s3에 요청한다 (cloudfront에 리사이징한 이미지가 캐싱되어 있으면 바로 주고 과정은 끝남)

3. cloudfront에 설정해놓은 origin response 트리거로 lambda 함수가 호출된다

4. 요청한 url(&query) 로 이미지를 리사이징하고 리사이징된 콘텐츠는 cloudfront가 캐싱한다.

5. url을 요청한 사용자는 해당 이미지를 받아 본다.

출처: 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

 

그럼 아키텍처대로 구현해보자.

 

1. s3 버킷 만들기

처음에 리전 선택 잘못했다가 다시 만들었다. 버킷 사용자가 주로 위치한 지역과 가까운 리전을 선택해야 데이터 전송 시간을 줄일 수 있다. cloudfront로 데이터를 캐싱해서 주더라도 첫 요청은 s3에서 응답하기 때문에 사용자가 많은 곳으로 리전 선택을 해주도록 하자.

 

2. cloudfront 배포 생성하기

origin domain에 위에서 만든 s3를 설정한다.

-> cloudfront가 콘텐츠를 가져올 오리진(원본) 서버를 설정하는것.

s3 버킷, ec2 인스턴스 또는 다른 http 서버일 수 있고 origin 의 위치와 성격에 따라 성능에 영향을 미칠수 있다.

원본 엑세스 제어 설정을 하면 s3로의 직접 접근이 막히고 cloudfront를 통한 접근만 가능해진다.

create new OAC(Origin access control) 를 만들어준다

OAC 생성

위의 과정은 cloudfront 의 OAC(원본 액세스 제어)로 s3 원본을 보호하게 해주는데 요청 응답 플로우는 다음과 같다.

1. 클라이언트가 HTTPS 요청을 Cloudfront로 보냄

2. Cloudfront 엣지 로케이션이 요청을 수신하고 요청 객체가 아직 캐시되지 않은 경우

  OAC 서명 프로토콜을 사용해 요청에 서명.

3. S3는 요청에 응답 (올바른 서명이면 승인, 아니면 거부)

-> 위의 서명 요청을 선택하면 위 과정 중 2번에서 CloudFront IAM 서비스 보안 주체가 수신 요청 발생 시  Authorization 헤더에 서명한다. (관련 docs)

OAC 워크플로우 (출처: https://aws.plainenglish.io/b1a9f49dfab3)

 

 

WAF 방화벽

그리고 중간에 WAF 선택 란이 있는데 test용이면 비활성화 하면된다.

aws 설명에 혹해서 활성화했는데 웹 사이트 이용자가 거의 없는데 WAF비용만 한달에 $10 정도씩 나가서 비활성화했다..  

-> WAF은 SQL 인젝션, XSS, 파일 포함 공격, 악성 봇 차단, DDoS 공격에 대한 방어를 해준다.

 

+ 좀더 알아보니 이런 방어를 cloudfront 이후에 해서 url 요청 자체는 모두 받아들인다. 예를들어 DDos 같은 경우 Origin에 대한 요청은 방지하지만 cloudfront 요청은 무수히 발생하고 WAF도 어쨌든 요청에 대한 deny response가 무수히 발생하는 상황이라 해당 요금이 과도하게 발생할 여지가있다. 

추가적인 내용은 WAF 보안 관련해서 알아보다가 매우 도움이 되었던 블로그를 링크해놓겠다. 

Cloudfront 이후에 동작하는 WAF

 

https://changmyeong.tistory.com/77

 

AWS CloudFront에서 CloudFlare로 이관한 후기

2023년 6월, 여러 게임의 편의 기능을 제공하며 유저를 조금씩 모아보겠다는 취지로 사이드 프로젝트를 개발하여 오픈했다. 개발 시간은 1.5일 정도 걸렸지만 오픈한지 한 달이 되던 때에 하루에 7

changmyeong.tistory.com

 

3 cloudfront 배포 후 해당 정책 s3 버킷 정책에 등록

해당 과정을 진행해야 cloudfront를 통해 s3 에 접근할 수 있다.

cloudfront 의 정책 복사
s3 > 권한 > 버킷정책에 붙여넣어준다

 

이렇게하면 cloudfront를 통해 s3 이미지에 접근할 수 있다.

 

4. cloudfront 캐시 정책 생성

쿼리 문자열에 따라 콘텐츠를 캐싱하기위해서는 cloudfront에 쿼리 문자열을 포함하도록 설정해야한다.

 

5. iam 

-> iam (identity and access management) 리소스에 대한 엑세스를 관리하고 제어하는 서비스로 iam을 통해 aws 리소스에 대한 권한을 세밀하게 설정하고 관리할 수 있다. 우린 아래 방법 중 2) 역할을 가진 iam 이 필요하다.

 

1) 사용자에게 권한 부여

 - aws 계정의 리소스에 액세스할 수 있는 개별 사용자를 생성할 수 있다. (사용자에게 필요한만큼 권한 부여)

 - JSON 형식의 정책을 통해 사용자의 권한을 정의한다.

2) 역할 (role)

 - 주로 ec2 인스턴스나 lambda 함수와 같은 aws 서비스가 다른 aws 리소스에 액세스 할 때 사용.

 

iam 역할을 생성한다.
권한은 AWSLambdaExecute를 선택한다.

lambda 관련 권한 정책이 다양한데 그중 AWSLambdaExecute는 lambda 함수를 호출할 수 있는 권한을 포함하고, s3의 GET에 대한 액세스를하게 해준다. (관련 docs)

 

iam 생성 후 신뢰관계를 편집해서 lambdaedge 에 대한 권한을 추가해준다. (관련 docs)

 

이러면 lambda 함수 실행시 우리에게 필요한 iam이 완성되었다.

다음편 serverless 프레임워크로 lambda함수 배포하기 

'aws' 카테고리의 다른 글

AWS cloudfront & lambda@edge 로 이미지 리사이징-2  (2) 2024.08.29

+ Recent posts