[Sprint] 서버리스 사진첩

# 학습 목표

  • 이미지가 업로드되면, 원본과 별도로 썸네일을 생성하고, 이를 별도의 버킷에 저장해야 합니다.
  • 아래 과제 제출 방법을 참고해 GitHub에 제출합니다.

# 해결 과제

  • 과제를 달성하면, S3 이벤트가 SQS로 전송되게 만들고, SQS로부터 이벤트를 받아 람다가 실행하게 만들어봅시다.
  • 썸네일 생성이 완료되면, 메일로 해당 썸네일 URL과 함께 전송이 되게 만들어봅시다.
  • S3의 Pre-signed URL 기능을 이용하여, 업로드 전용 URL을 획득하고, 이를 통해 이미지를 S3 업로드할 수 있게 만들어봅시다.

# 실습 자료

  1. sam init 명령을 이용해 Quick Start Template으로부터 Standalone function을 하나 생성합니다.
  2. lambda 함수의 파라미터를 정의합니다. 이는 이벤트 소스로부터 트리거가 발생했을 때 이벤트의 형태를 확인하기 위함입니다. 다음과 같이 코드를 작성합니다.
exports.helloFromLambdaHandler = async (event, context) => {
    console.log(event)

    console.log(context)

    return 'Hello from Lambda!';
}

# 과제 항목별 진행 상황

1. Lambda 함수 생성

javascript 코드 작성

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

const sharp = require('sharp');

exports.helloFromLambdaHandler = async (event, context) => {
    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/+/g, ' '));    

    const params = {
        Bucket: bucket,
        Key: key,
    };

    // 원본 버킷으로부터 파일 읽기    
    const s3Object = await s3.getObject(params).promise();
    
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다.
    const data = await sharp(s3Object.Body).resize(200).jpeg({ mozjpeg: true }).toBuffer()
  
    // param 버킷을 대상 버킷으로 변경
    params.Bucket = process.env.BUCKET_TARGET

    // 대상 버킷으로 파일 쓰기
    const result = await s3.putObject({
        ...params,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()
}

build & deploy

oh@devops  ~/codeStates/sam-image-app  sam build            
Starting Build use cache
Manifest file is changed (new hash: da81938432ee64c3c397dcbd39b15e4b) or dependency folder (.aws-sam/deps/d3b653c2-eee4-4088-8292-77f8ae83bc69) is missing for (helloFromLambdaFunction), downloading dependencies and copying/building source
Building codeuri: /home/oh/codeStates/sam-image-app runtime: nodejs14.x metadata: {} architecture: x86_64 functions: helloFromLambdaFunction
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrcAndLockfile
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall
Running NodejsNpmBuilder:CleanUp
Running NodejsNpmBuilder:CopyDependencies
Running NodejsNpmBuilder:CleanUpNpmrc
Running NodejsNpmBuilder:LockfileCleanUp
Running NodejsNpmBuilder:LockfileCleanUp

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided


oh@devops  ~/codeStates/sam-image-app  sam deploy --guided       

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-image-app]: 
        AWS Region [ap-northeast-2]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [Y/n]: n
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: n
        Capabilities [['CAPABILITY_IAM']]: 
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: N
        Save arguments to configuration file [Y/n]: n

        Looking for resources needed for deployment:

        Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-qo0co9c7ixez
        A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False
        Uploading to sam-image-app/0761243950ce57d64feac4a3f69d0c92  19550613 / 19550613  (100.00%)

        Deploying with following values
        ===============================
        Stack name                   : sam-image-app
        Region                       : ap-northeast-2
        Confirm changeset            : False
        Disable rollback             : False
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-qo0co9c7ixez
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles             : {}

Initiating deployment
=====================

        Uploading to sam-image-app/ab80d8e45bac96e41d1a0abc18153d11.template  673 / 673  (100.00%)


Waiting for changeset to be created..

CloudFormation stack changeset
-----------------------------------------------------------------------------------------------------------------------------------------
Operation                          LogicalResourceId                  ResourceType                       Replacement                      
-----------------------------------------------------------------------------------------------------------------------------------------
* Modify                           helloFromLambdaFunction            AWS::Lambda::Function              False                            
-----------------------------------------------------------------------------------------------------------------------------------------


Changeset created successfully. arn:aws:cloudformation:ap-northeast-2:057440442371:changeSet/samcli-deploy1683701668/a7c43724-60fd-4c88-98c2-d3808a88326b


2023-05-10 15:54:34 - Waiting for stack create/update to complete

CloudFormation events from stack operations (refresh every 5.0 seconds)
-----------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                     ResourceType                       LogicalResourceId                  ResourceStatusReason             
-----------------------------------------------------------------------------------------------------------------------------------------
UPDATE_IN_PROGRESS                 AWS::Lambda::Function              helloFromLambdaFunction            -                                
UPDATE_COMPLETE                    AWS::Lambda::Function              helloFromLambdaFunction            -                                
UPDATE_COMPLETE_CLEANUP_IN_PROGR   AWS::CloudFormation::Stack         sam-image-app                      -                                
ESS                                                                                                                                       
UPDATE_COMPLETE                    AWS::CloudFormation::Stack         sam-image-app                      -                                
-----------------------------------------------------------------------------------------------------------------------------------------


Successfully created/updated stack - sam-image-app in ap-northeast-2

2. S3 버킷 생성

소스 버킷 생성

타겟 버킷 생성

타겟 버킷 ACL 활성화

3. Lambda 트리거 추가

4. Lambda 실행 역할에 권한 정책 추가

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "S3GetObject",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::bighead-image-src/*"
        },
        {
            "Sid": "S3PutObject",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::bighead-image-tg/*"
        }
    ]
}

5. 테스트

이미지 업로드

Lambda 실행 확인

썸네일 이미지 생성 확인


# Advanced Challenge

🔥 S3 이벤트가 SQS로 전송되게 만들고, SQS로부터 이벤트를 받아 람다가 실행하게 만들어봅시다.

1. SQS 대기열 생성

2. S3 이벤트 알림 추가

3. Lambda 실행 역할에 권한 정책 추가

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "VisualEditor",
			"Effect": "Allow",
			"Action": [
				"sqs:ReceiveMessage",
				"sqs:DeleteMessage",
				"sqs:GetQueueAttributes"
			],
			"Resource": "arn:aws:sqs:ap-northeast-2:057440442371:bighead-image-convert"
		}
	]
}

4. Lambda 트리거 추가

5. Lambda 소스 코드 수정

트리거를 SQS로 변경 시, S3 Event 관련 정보가 event.Records[0].body.Records[0].s3 안으로 들어온다.

const aws = require('aws-sdk')
const region = process.env.AWS_REGION

aws.config.update(region);

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

const sharp = require('sharp')

exports.helloFromLambdaHandler = async (event, context) => {
    const body = JSON.parse(event.Records[0].body)
    const s3Event = body.Records[0].s3

    const bucketSrc = s3Event.bucket.name
    const bucketTg = process.env.BUCKET_TARGET

    const key = decodeURIComponent(s3Event.object.key.replace(/+/g, ' '))

    const s3Params = {
        Bucket: bucketSrc,
        Key: key,
    }

    // 원본 버킷으로부터 파일 읽기    
    const s3Object = await s3.getObject(s3Params).promise()
    
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다.
    const data = await sharp(s3Object.Body).resize(200).jpeg({ mozjpeg: true }).toBuffer()
  
    // param 버킷을 대상 버킷으로 변경
    s3Params.Bucket = bucketTg

    const result = await s3.putObject({
        ...s3Params,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()
}

🔥 썸네일 생성이 완료되면 Amazon SNS를 활용하여, 메일로 해당 썸네일 URL과 함께 전송이 되게 만들어봅시다.

1. SNS 주제 생성

2. SNS 구독 생성

3. Lambda 실행 역할에 권한 정책 추가

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor",
            "Effect": "Allow",
            "Action": [
                "sns:Publish"
            ],
            "Resource": "arn:aws:sns:ap-northeast-2:057440442371:bighead-image-email"
        }
    ]
}

4. Lambda 소스 코드 수정

const aws = require('aws-sdk')
const region = process.env.AWS_REGION

aws.config.update(region);

const s3 = new aws.S3({ apiVersion: '2006-03-01' })
const sns = new aws.SNS({apiVersion: '2010-03-31'})

const sharp = require('sharp')

exports.helloFromLambdaHandler = async (event, context) => {    
    const body = JSON.parse(event.Records[0].body)
    const s3Event = body.Records[0].s3

    const bucketSrc = s3Event.bucket.name
    const bucketTg = process.env.BUCKET_TARGET

    const key = decodeURIComponent(s3Event.object.key.replace(/+/g, ' '))

    const s3Params = {
        Bucket: bucketSrc,
        Key: key,
    }

    // 원본 버킷으로부터 파일 읽기    
    const s3Object = await s3.getObject(s3Params).promise()
    
    // 이미지 리사이즈, sharp 라이브러리가 필요합니다.
    const data = await sharp(s3Object.Body).resize(200).jpeg({ mozjpeg: true }).toBuffer()
  
    // param 버킷을 대상 버킷으로 변경
    s3Params.Bucket = bucketTg

    const result = await s3.putObject({
        ...s3Params,
        ContentType: 'image/jpeg',
        Body: data,
        ACL: 'public-read'
    }).promise()

    const snsParams = {        
        TopicArn: process.env.SNS_TOPIC_ARN,
        Subject: 'Thumbnail have been extracted from the image.',
        Message: `Thumbnail URL: https://${bucketTg}.s3.${region}.amazonaws.com/${key}`
    }
    
    // SNS 메시지 게시
    await sns.publish(snsParams).promise()

}

5. 테스트

이메일 전송 확인


🔥 S3의 Pre-signed URL 기능을 이용하여, 업로드 전용 URL을 획득하고, 이를 통해 이미지를 S3 업로드할 수 있게 만들어봅시다.

1. Lambda 소스 코드 수정

const aws = require('aws-sdk')
const region = process.env.AWS_REGION

aws.config.update(region);

const s3 = new aws.S3({ apiVersion: '2006-03-01' })
const sns = new aws.SNS({apiVersion: '2010-03-31'})

const sharp = require('sharp')
const axios = require('axios')

exports.helloFromLambdaHandler = async (event, context) => {    
    const body = JSON.parse(event.Records[0].body)
    const s3Event = body.Records[0].s3

    const bucketSrc = s3Event.bucket.name
    const bucketTg = process.env.BUCKET_TARGET

    const key = decodeURIComponent(s3Event.object.key.replace(/+/g, ' '))

    const s3Params = {
        Bucket: bucketSrc,
        Key: key,
    }

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

    // 대상 버킷으로 파일 쓰기
    await axios.put(presignedURL, data, {
        headers: {
            'Content-Type': 'image/jpeg',
            'x-amz-acl': 'public-read'
        }
    })

    // SNS 메시지 게시
    const snsParams = {        
        TopicArn: process.env.SNS_TOPIC_ARN,
        Subject: 'Thumbnail have been extracted from the image.',
        Message: `Thumbnail URL: https://${bucketTg}.s3.${region}.amazonaws.com/${key}`
    }
    
    await sns.publish(snsParams).promise()

}

2. 테스트

변환된 이미지 업로드 확인


# TROUBLE SHOOTING LOG

💡 S3 이벤트 → SNS 구성시 원하는 응답을 받지 못함

원인

S3에 생성되는 객체에 대한 이벤트만 SNS로 알림을 하기 때문에 사진 URL 응답을 받을 수 없음

해결 방안

위와 같은 방향으로 다시 코드를 작성하여 해결


💡 SQS를 트리거로 하는 Lambda가 지속 호출되는 이슈

원인

Lambda 함수 코드 오류 발생으로 인해 호출이 재시도 됨.

해결 방안

메시지 보존 기간을 조정하여 SQS 대기열에서 처리되지 않은 메시지 삭제 추후 데드 레터 큐(DLQ) 의 사용을 고려해 볼 필요가 있음.

[레퍼런스]

Amazon SQS에서 Lambda 사용 – 실패한 호출에 대한 백오프 전략

Lambda 호출 모드 비교 – SQS 재시도 이해


💡 Pre-signed URL을 사용하여 S3 업로드를 하였을 때, 해당 객체 URL에 접속되지 않는 이슈

원인

Pre-signed URL 생성 및 S3 업로드 시 ACL을 지정하지 않음.

해결 방안

public-read ACL 지정

// get presignedURL
const presignedURL = s3.getSignedUrl('putObject', {
    Bucket: bucketTg,
    ContentType: 'image/jpeg',
    ACL: 'public-read',
    Key: key,
    Expires: 60
})

// S3 putObject
await axios.put(presignedURL, data, {
    headers: {
        'Content-Type': 'image/jpeg',
        'x-amz-acl': 'public-read'
    }
})

#References

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/grant-destinations-permissions-to-s3.html

https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/API/API_PutObject.html

Leave a Comment