[TROUBLESHOOTING] 서버리스 사진첩

Son_Doobu96·2023년 2월 3일
0

sprint Troubleshooting

목록 보기
2/7

과제의 목표

서버리스 사진첩 서비스는, 여느 클라우드 사진 저장 서비스들처럼 단순히 사진을 업로드하는 것 외에도, 인증 기능과 썸네일 생성 기능을 제공합니다.

최소 요구 조건

  • 이미지가 업로드되면, 원본과 별도로 썸네일을 생성하고, 이를 별도의 버킷에 저장해야 합니다.
  • 썸네일 이미지는 가로 200px의 크기를 가집니다.
  • 썸네일을 저장할 별도의 버킷은 람다 함수의 환경 설정으로 구성되어야 합니다.
  • 썸네일 생성이 완료되면, 메일로 해당 썸네일 URL과 함께 전송이 되어야 합니다.
  • Amazon SNS를 활용합니다.

시작 세팅

sam init을 통해 Quick Start Template으로부터 Standalone function을 선택했다.
아래는 Standalone function에 대한 정의다.

독립 실행형 함수는 특정 작업을 수행하고 클래스나 객체의 일부가 되지 않고 독립적으로 실행될 수 있는 독립적인 코드 조각입니다. 다양한 프로그래밍 언어로 정의할 수 있으며 입력 매개변수를 수락하고 출력 값을 반환할 수 있습니다. 독립 실행형 기능은 코드를 구성하고 재사용하는 방법을 제공하여 유지 관리 및 디버그를 더 쉽게 만듭니다.


처음 만나게 된 문제

시작 세팅을 완료한 후 Lambda 함수에 대해 간단한 정의 후 빌드 및 배포를 진행했다.

시작 코드
exports.helloFromLambdaHandler = async (event, context) => {
    console.log(event)

    console.log(context)

    return 'Hello from Lambda!';
}

몇 몇 권한을 추가 한 후 이미지를 업로드 할 S3 버킷을 생성해 트리거로 연결해주었고 테스트까지는 어렵지 않게 성공할 수 있었다.

하지만 CloudWatch에 s3 업로드 후 Lambda 실행에 대한 로그 기록이 쌓이지 않는 것을 확인했고
Lambda 함수가 실행되면 로그 기록이 생성되고 추가 된다는 것을 확인 한 후 문제를 두 가지로 압축할 수 있었다.

  1. Lambda 함수 자체에 문제가 있다.
  2. S3 버킷과 Lambda 사이의 문제다.

해당 문제를 해결하기 위해 꽤 오랜시간을 썼는데... 일단 테스트는 문제 없이 진행되었기 때문에 1번은 아니라고 생각하고 2번을 전제로 문제를 해결하기 위해 노력했다.

문제는 Lambda를 뒤져보면서 아무렇지 않게 지나친 그 곳에 있었다.
문제를 해결하지 못해 엔지니어님께 문의 한 결과 Lambda의 실행권한에 대한 문제일 것 같다는 이야기를 전달받았고 Lambda의 실행권한에서 S3에 접근할 수 있는 권한을 추가한 후 문제를 해결할 수 있었다.


두 번째 만나게 된 문제

두 번째 문제는 S3 버킷에 업로드한 이미지를 리사이징하여 새로운 버킷에 자동으로 업로드하는 과정에서 일어났다.

실습의 진행을 위해 간단한 Snippet code가 주어졌는데.

// 원본 버킷으로부터 파일 읽기
const s3Object = await s3.getObject({
  Bucket: 원본_버킷_이름,
  Key: 업로드한_파일명
}).promise()

// 이미지 리사이즈, sharp 라이브러리가 필요합니다.
const data = await sharp(s3Object.Body)
    .resize(200)
    .jpeg({ mozjpeg: true })
    .toBuffer()

// 대상 버킷으로 파일 쓰기
const result = await s3.putObject({
  Bucket: 대상_버킷_이름, 
  Key: 업로드한_파일명과_동일,
  ContentType: 'image/jpeg',
  Body: data,
  ACL: 'public-read'
}).promise()

해당 코드와 S3 트리거를 활용해 Lambda 함수를 호출하는 AWS 공식문서를 활용해 기존 파일을 아래와 같이 수정했다.

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });

exports.helloFromLambdaHandler = async (event, context) => {
    console.log(event)

    console.log(context)

    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));


    // 원본 버킷으로부터 파일 읽기
    const s3Object = await s3.getObject({
        Bucket: bucket,
        Key: key
    }).promise()
  
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다.
    const data = await sharp(s3Object.Body)
      .resize(200)
      .jpeg({ mozjpeg: true })
      .toBuffer()
  
    // 대상 버킷으로 파일 쓰기
    const result = await s3.putObject({
        Bucket: "리사이징 버킷 이름", 
        Key: key,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()
    
    return result;

배포 후 콘솔에서 테스트를 진행했을때 처음 마주하게 된 문제는 key값을 정의 할 수 없다는 것이었다.
위의 코드에서 소스를 불러올 버킷과 이미지의 이름은 정의하였지만 그 불러온 이미지를 리사이징하여 보내줄 버킷과 key가 설정되지 않아서 발생한 문제였다.
코드를 아래와 같이 수정함으로써 해당 문제는 해결할 수 있었다.
트리거를 사용해 썸네일을 생성하는(리사이징 할 수 있는) AWS 공식문서

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });

exports.helloFromLambdaHandler = async (event, context) => {
    console.log(event)

    console.log(context)

    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    const rzbucket = bucket + "-resize"
    const rzkey = key + "-resize" 


    // 원본 버킷으로부터 파일 읽기
    const s3Object = await s3.getObject({
        Bucket: bucket,
        Key: key
    }).promise()
  
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다.
    const data = await sharp(s3Object.Body)
      .resize(200)
      .jpeg({ mozjpeg: true })
      .toBuffer()
  
    // 대상 버킷으로 파일 쓰기
    const result = await s3.putObject({
        Bucket: rzbucket, 
        Key: rzkey,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()

  return result;
}

세 번째 만나게 된 문제

코드를 수정한 후 다시 빌드하여 배포를 진행했다. 세번째 문제가 날 기다리고 있었다 ㅎㅎ
Sharp 모듈을 찾을 수 없다는 내용의 문제였다.
사실 이 문제는 큰 문제는 아니었다. 오류 내용? 알고 있고 해결방법? 역시 알았다.

처음 시도한 방법

페어와 함께 작업을 진행하면서 처음 이 문제의 해결방법에 대해 Stackoverflow 형님들의 이 해결 사례를 보면서 문제를 해결하려 했다.

우리의 첫 생각은 이거였다.

  • 로컬에서 함수를 작동시킬게 아니라!
  • Lambda에서 작동시킬 거니까!!
  • Lambda는 linux 환경에서 작동하니까!!

이렇게 해줘야 하지 않을까? 라고 생각하고 문제에 접근했다.

npm install --arch=x64 --platform=linux sharp

그리고 소스코드에 
const sharp = require('sharp');를 추가로 선언해줬다.

이후는 혼돈의 카오스였다. 내 페어는 문제 없이 작동했지만 난 아니었다. 계속해서 Cannot find module 에러를 생산해냈다... 정말 머리가 복잡해져서 서로 코드도 비교해가면서 문제를 찾아보려 했지만 찾지를 못했다.

해결한 방법?

일단 먼저 해결한 방법에 대해 이야기 하자면 $ npm i sharp를 통해 의존성을 설치 해주니 문제가 해결됐다...

문제 원인에 대한 고민

처음에 Lambda에서 작동시킬 거니까 무조건 linux 환경에 맞추어서 구성을 해야겠다는 생각을 했던 게 지금에 와서는 문제의 원인이 되지 않았나 싶다.
나의 페어는 mac을 사용하고 나는 window를 사용하는데 사용자의 OS 환경에 미치는 영향을 고려해가면서 문제를 바라볼 필요가 있다는 생각이 들었다.


네 번째 만나게 된 문제

다음 문제는 AccessDenied 였다. 이 문제는 공식문서에 그 답이 완전히 나와있었다.
Amazon S3 트리거를 사용하여 썸네일 생성 AWS 공식문서

여기에 위에 내가 작성한 코드를 실행시키기 위한 IAM 정책을 생성할 필요가 있다고 이미 적혀 있었는데.... 제대로 안보고 넘어갔다..

아무튼 공식 문서를 통해 Lambda 실행 역할에 만든 정책을 추가한 후 문제를 해결할 수 있었다.


다섯 번째 만나게 된 문제

여기서부턴 진짜 힘들었다. 오늘은 금요일 하기도 너무 싫은데 하필 금요일이다. 와우
거기다 Console의 테스트는 계속해서 통과하는데 로그가 쌓이지 않는다......
그리고 S3에 이미지를 업로드해도 리사이징 버킷에 이미지가 생성되지를 않았다.
하지만 테스트는 계속해서 통과하는 아이러니한 상황이었다.

뭐가 문제일지 정말 여러 방면으로 찾아봤지만 에러메세지 조차 남겨주질 않으니 찾기가 너무 힘들었다 ㅠㅠ

처음 시도한 방법

함수의 문제인지를 확인하기 위해 디버깅을 해봤다. 내가 디버깅에 대한 지식은 전무하기에...
페어의 도움을 받아 디버깅을 진행할 수 있었다.

(async () => {
    const a = await require ('./handlers/hello-from-lambda').helloFromLambdaHandler()
    console.log(a)
})();

디버깅을 위한 app.js 파일을 생성 후 위의 코드를 넣었고 소스코드를 아래와 같이 수정했다.

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
const sharp = require('sharp');

exports.helloFromLambdaHandler = async (/*event, context*/) => {
    console.log(JSON.stringify(event))

    console.log(JSON.stringify(context))

    const bucket = <"소스를 불러올 버킷 이름">
    const key = <"소스를 불러올 이미지 ID">
    const rzbucket = bucket + "-resize"
    const rzkey = "resize-" + key


    // 원본 버킷으로부터 파일 읽기
    const s3Object = await s3.getObject({
        Bucket: bucket,
        Key: key
    }).promise()
  
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다.
    const data = await sharp(s3Object.Body)
      .resize(200)
      .jpeg({ mozjpeg: true })
      .toBuffer()
  
    // 대상 버킷으로 파일 쓰기
    const result = await s3.putObject({
        Bucket: rzbucket, 
        Key: rzkey,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()
}

이후 로컬에서 접근해야 하기에 S3 버킷을 퍼블릭으로 전환 및 정책 생성 후 $ node app.js로 디버깅을 진행했다.

그리고 이미지 리사이징에 성공했다... ㅠㅠ 일단 다행히도 함수는 문제가 없었다.

어떻게 해결했나?

디버깅까지 진행 후에도 문제는 여전했다. S3에 이미지를 업로드해도 로컬에서 처럼 리사이징이 되질 않았다.
이제는 진짜 방법을 모르겠어서 멍해져갈때쯤 엔지니어님이 갑자기 방문해주셨다. 디버깅을 통해 로컬에서 함수에는 문제가 없는 상황을 말해드렸고 배포를 통해 콘솔에서의 테스트에서도 문제가 없는 상황을 말씀드렸다.

엔지니어님께서 여러 화면을 슥 슥 보시더니 .jpg로 되어 있는 확장자를 가르켜 주셨다.

내가 구성한 S3 트리거의 구성 정보이다.

.jpeg 확장자를 올려야 이미지를 .jpg로 계속 올리고 있었던 것이다....
그러다보니 트리거에서 해당 조건에 부합하지 않으니 Lambda 함수를 실행시키지 않았던 것이다.

그렇게 3시간은 'e' 하나에 날아갔다. 영상 편집자로 2년을 일하면서 수많은 jpg와 jpeg를 봤는데 그 차이 한번을 검색해보지 않은 스노우볼이 오늘의 3시간을 날렸다...

마지막 문제

마지막은 리사이징한 이미지를 이메일을 통해 보내주는 작업을 구성하는 일이었다.
AWS SNS를 통해 해당 구성에 대한 세팅을 마쳤다.

이후 메일이 전송되어 확인을 했는데

이렇게 복잡한 문자로 오는 것을 확인할 수 있었다. 이 문제를 콘솔에서 해결 할 수 있는 방법을 찾다가 도저히 찾지 못해서 소스코드를 추가해 해당 문제를 해결할 수 있었다.

const region = "ap-northeast-2"

    const sns = new aws.SNS(region);
    const massage = await sns.publish({
        Message: "URL : " + `https://${rzbucket}.s3.${region}.amazonaws.com/${rzkey}`,
        Subject: "썸네일 도착",
        TopicArn: <"TopicArn">
    }).promise()
}

아직 해결되지 않은 문제?

수정한 소스코드를 배포한 함수로 메일이 하나 오고 이전 소스코드로도 메일이 오는 현상이 있었다.
처음에는 조금 당황했으나 이전 소스코드의 아래의 지워진 링크를 클릭하여 구독을 취소하고
맨 처음에 온 메일로 다시 구독을 수락하니 해당 문제를 해결할 수 있었다.

진짜 마지막 문제!

CloudWatch로 들어오는 로그가 객체 형식으로 들어오다 보니 너무 보기가 불편했다.
마지막을 소스코드를 수정해 JSON 형식으로 로그가 들어올 수 있게 수정하였다.

최종 소스코드

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
const sharp = require('sharp');
const region = "ap-northeast-2"

exports.helloFromLambdaHandler = async (event, context) => {
    console.log(JSON.stringify(event))

    console.log(JSON.stringify(context))

    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    const rzbucket = bucket + "-resize"
    const rzkey = "resize-" + key


    // 원본 버킷으로부터 파일 읽기
    const s3Object = await s3.getObject({
        Bucket: bucket,
        Key: key
    }).promise()
  
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다. 
    const data = await sharp(s3Object.Body)
      .resize(200)
      .jpeg({ mozjpeg: true })
      .toBuffer()
  
    // 대상 버킷으로 파일 쓰기
    const result = await s3.putObject({
        Bucket: rzbucket, 
        Key: rzkey,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()

    const sns = new aws.SNS(region);
    const massage = await sns.publish({
        Message: "URL : " + `https://${rzbucket}.s3.${region}.amazonaws.com/${rzkey}`,
        Subject: "썸네일 도착",
        TopicArn: <"TopicArn">
    }).promise()
}

이제 로그도 이쁘게 들어온다!


이번 트러블 슈팅을 하면서 느낀 점

디버깅의 중요성을 깨닫게 되었다. 소스코드를 배포하고 콘솔에서 테스트 하는 작업이 여간 불편한 게 아니었다.. 그리고 문제의 원인의 가짓수를 지워나가는 데에도 정말 도움이 안됐던것 같다.
디버깅을 앞으로 잘 할 수 있는 능력을 키워야 겠다는 생각이 강하게 들었다.

그리고 조금 더 꼼꼼하게 공식문서를 보는 능력을 키워야겠다는 생각이 들었다.

마지막으로 ChatGTP를 잘 활용하면 좋을것 같았다. AWS SNS 서비스를 nodejs로 어떻게 표현해야 할까라고 질문을 했었는데
위의 사진처럼 정말 자세하게 알려주었다. AI에 잘 질문하는 게 정말 앞으로는 중요해 지겠다는 생각이 들었다.

profile
DevOps를 꿈꾸는 엔지니어 지망생!

0개의 댓글