티스토리 뷰

주의 사항

AWS Lambda AWS API Gateway 사용하기 전에 알아야 것이 있다.

 

목적

Client에서 Server Content-type:multipart/form-data 이미지 파일을 포함한 파라미터를 POST 요청

Server에서 파라미터를 파싱하여 데이터베이스의 CRUD S3 이미지 파일 업로드

S3 이미지 파일 업로드 트리거 되어 리사이징 후 업로드

 

 

환경

API Gateway 1

Lambda 2개 / 구성 언어: Nodejs

S3 1개 / 버킷 1, 디렉터리 2

RDS 1개 / database-server: Mysql

Endpoint 1개 / S3

 

 

동작

 

구축

모든 권한을 가지고 있는 어드민으로 진행

 

 

Upload 람다

  • 생성
    • AWS 콘솔 로그인 > 서비스 > 컴퓨팅 Lambda > 함수 생성 > 새로 작성, 함수 이름 입력, 런타임 Node.js 12.x > 함수 생성
  • 함수 코드 업로드
    • 람다 페이지에서는 npm 명령어 수행 불가, 로컬에서 zip 파일로 올려 작업 진행
    • 10MB 넘는 경우 S3 zip 파일을 업로드 url 입력
  • 환경 변수
    • 개발계와 운영계를 나눌 생각이면 환경변수를 추가하면 되고 변수 키와 값은 임의로 설정
    • 예시) 환경 변수 > 편집 > key: NODE_ENV, value: prod(또는 dev)
  • 기본 설정
    • 메모리, 제한시간 설정 가능
    • 예시) 기본 설정 > 편집 > 메모리 1024MB, 제한 시간 5
  • VPC
    • 람다에서 RDS 연결하기 위해선 VPC 설정과 권한이 필요
    • VPC  > 편집 >  RDS VPC, 서브넷, 보안 그룹을 입력
  • 실행 역할
    • 역할 이름에 권한 추가
    • AWSLambdaVPCAccessExecutionRole: VPC 사용 권한 부여
      • Role > 권한 > 정책 연결 > AWSLambdaVPCAccessExecutionRole
    • AWSLambdaBasicExecutionRole: CloudWatch 로그 그룹에 로그 사용 권한(자동 생성)
    • 인라인 정책 추가 S3버킷 권한 추가
    • 신뢰 관계

 

 

Upload API Gateway

  • 생성
    • 네트워킹 콘텐츠 전송 > API Gateway > API 생성 > REST API 구축 > 프로토콜 REST, API, API 이름 입력, 엔드포인트 유형 지역 > API 생성
  • 리소스
    • 요청을 받을 URI 정의, 람다를 등록
      • 작업 > 리소스 생성 > 리소스 이름, 리소스 경로 입력 > 리소스 생성
      • 생성된 URI > 작업 > 메서드 생성 > POST > 확인 > 통합 유형: 람다 함수, 람다 함수: 존재하는 람다 함수 입력 > 저장 > 람다 함수에 대한 권한 추가 > 확인
    • 통합 요청
      • 매핑 템플릿(클라이언트가 보낸 파라미터를 매핑하는 템플릿) 생성
        • 매핑 템플릿 > 정의된 템플릿이 없는 경우(권장) > 매핑 템플릿 추가 > multipart/form-data 입력 > 확인 > 하단 템플릿 생성 셀렉트 박스, 메서드 요청 패쓰스루 선택 >. “body-json” : $input.json(‘$’) “body” : “$input.body” 변경
  • 설정
    • 이진 미디어 형식
      • 미디어 파일을 바이트 손실없이 람다에 전달하기 위해 바이너리로 변경
      • 이진 미디어 형식 추가 > multipart/form-data > 확인
  • 스테이지
    • API Gateway 배포한 API 정보를 확인하는 메뉴이며 API Gateway 설정을 변경하면 배포하여 적용
    • 메서드 배포하는 방법
      • 리소스 > 메서드 선택 > 작업 > 배포 스테이지: 스테이지(기존 스테이지 가능), 이름, 설명, 배포 설명 입력 > 배포
    • CloudWatch 설정
      • API > 로그/추적 > CloudWatch 설정 > 로그 활성화 체크, 로그 수준 INFO, 전체 요청/응답 데이터 로깅 체크, 세부 지표 활성화 체크 > 변경 사항 저장
    • 배포 기록
      • Git log처럼 조회 가능하며 배포 설명이 나옴, 배포 시점을 변경하여 버전 관리 가능
    • 메서드
      • URL 호출의 URL POST 요청을 있음
  • 사용자 지정 도메인 이름
    • 자동 생성된 메서드 URL 변경하고 싶을 사용
    • 도메인은 임의로 생성 가능해 보이나 소유한 도메인을 이용한 서브 도메인을 만드는 것이 나을 같음
    • 생성
      • 도메인 이름 입력 > 구성 지역, TLS 1.2 > ACM 인증서 선택 > 생성
      • API 매핑 구성 > API, 스테이지, 경로(선택 사항) 작성 > 저장
      • 생성된 도메인은 Route53에서 특정 도메인에 alias 돼야하며 “API Gateway 도메인 이름 입력하여 구성

 

 

엔드포인트

  • VPC 사용할 경우 인라인 정책만으로는 S3 연결할 없으므로 엔드포인트를 추가
  • 네트워킹 콘텐츠 전송 > VPC > 가상 프라이빗 클라우드 > 엔드포인트 > 엔드포인트 생성 > 서비스 범주: AWS 서비스, 서비스 이름: ~.s3, VPC 선택 > 엔드포인트 생성
  • S3 엔드포인트 정책은 수정할 경우 다른 서비스에서 정책 Resource 정의되지 않은 버킷을 이용할 없으므로 수정을 안하는 것을 권장

 

 

Resizing 람다

  • 업로드 람다 설정 VPC 제외한 설정 정책은 인라인 정책만 있으면
  • 업로드 람다의 버킷 디렉터리를 트리거 적용
  • 파일이 버킷 디렉터리에 업로드될 시 트리거가 발동하여 Resizing 람다가 수행

 

 

소스코드

Upload 람다

'use strict';

const Busboy = require('busboy');
const aws = require('aws-sdk');
const UUID = require('uuid');
const s3 = new aws.S3();
const mysql = require('sync-mysql');

exports.handler = async (event, context) => {
	//파싱
	const formData = await parse(event);
    
    //formData에서 파일데이터와 컨텐츠 타입을 파라미터로 upload 함수 콜
    const uploadPromise = await upload('파일 데이터', '컨텐츠 타입');
    
    //쿼리 수행
    mysqlConnection.query('쿼리');
    
    //응답 객체를 파라미터로 context.succeed 함수 콜
    context.succeed({status: 200, message: "success"});
}

const parse = (event) => new Promise((resolve, reject) => {
    const bodyBuffer = new Buffer(event.body.toString(), "base64");
    
    const busboy = new Busboy({
        headers: {
            'content-type': event.params.header['content-type'] || event.params.header['Content-Type']
        }
    });
    const formData = {};

    busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
        console.log('File [%s]: filename=%j; encoding=%j; mimetype=%j', fieldname, filename, encoding, mimetype);
        const chunks = [];

        file.on('data', data => {
            chunks.push(data);
        }).on('end', () => {
            formData[fieldname] = [filename, Buffer.concat(chunks), mimetype];
            console.log("File [%s] finished.", filename);
        });
    });

    busboy.on('field', (fieldname, value) => {
        console.log("[" + fieldname + "] >> " + value);
        formData[fieldname] = value;
    });

    busboy.on('error', error => {
        reject(error);
    });

    busboy.on('finish', () => {
        resolve(formData);
    });

    busboy.write(bodyBuffer, event.isBase64Encoded ? 'base64' : 'binary');
    busboy.end();
});

const upload = (fileData, contentType) => new Promise((resolve, reject) => {
    const bucket = '버킷명';
    const key = '버킷명 이하 저장 경로';
    const fileFullName = key + '/' + UUID.v4().replace(/\-/g, '');
    const params = {
        Bucket: bucket, 
        Key: fileFullName, 
        Body: fileData, 
        ContentType: contentType
    };
    
    s3.upload(params, (err, data) => {
       if(err){
           console.log(err);
           reject(err);
       }else{
           console.log(data);
           resolve(data);
       }
   });
});

const mysqlConnection = new mysql({
	host     : 'rds endpoint',
    user     : '유저명',
    password : '비밀번호',
    database : '데이터베이스 명'
});
  • 핵심 함수와 사용법만 표시
  • 콜백 방식을 지양하고 동기 방식으로 비동기 함수 처리를 수행 (sync-mysql, promise-await-sync)
  • 사용한 라이브러리: busboy, uuid, sync-mysql

 

Resizing 람다

const aws = require('aws-sdk');
const util = require('util');
const sharp = require('sharp');
const s3 = new aws.S3();

exports.handler = async (event, context) => {

    const imageSizeList = [1920, 640, 320];

    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, { depth: 5 }));

    // Origin Bucket Name.
    const originBucket = event.Records[0].s3.bucket.name;

    // Object key may have spaces or unicode non-ASCII characters.
    const originKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));

    // Change File Name
    const copyFileName = originKey.slice((originKey.lastIndexOf("/") - 1 >>> 0) + 2);
    const saveDirPath = '리사이징 경로';

    const copyBucket = '버킷명';
    let copyKey = '';

    // Sanity check: validate that source and destination are different buckets.
    if ((originBucket + '/' + originKey) === (copyBucket + '/' + copyKey)) {
        callback(new Error(`[FAIL]:${copyBucket}/${copyKey}:Source and destination buckets are the same.`));
        return;
    }

    console.log("===== downloadFileFromS3 =====");
    const originFileData = await downloadFileFromS3(originBucket, originKey);

    console.log("===== deleteFileInS3 =====");
    await deleteFileInS3(originBucket, originKey);

    console.log("===== resizing =====");
    await resizing(originFileData, imageSizeList, saveDirPath, copyFileName, copyBucket);

    console.log("===== Complete process of resizing images. =====");
    context.succeed({status: 200, content: "Complete resizing images."});
};

const downloadFileFromS3 = (originBucket, originKey) => new Promise((resolve, reject) => {
    const params = {
        Bucket: originBucket,
        Key: originKey
    };

    s3.getObject(params, (err, data) => {
        if(err){
            console.log(err);
            reject(err);
        }else{
            console.log(data);
            resolve(data);
        }    
    });
});

const resizing = (response, imageSizeList, saveDirPath, copyFileName, copyBucket) => new Promise(async (resolve, reject) => {
    for(var i in imageSizeList){        
        const size = imageSizeList[i];
        const resizeKey = `${saveDirPath}/${copyFileName}${size}`;

        console.log(`Running-ImageResize-${size}-${resizeKey}-${copyBucket}`);

        const buffer = await sharp(response.Body).resize(size).toBuffer();        
        await uploadFileToS3(copyBucket, resizeKey, buffer, response.ContentType).then((err, data) => {
            if(err){
                console.log(err);                
            }else{
                console.log(data);
            }
        });
    }

    resolve();
});

const uploadFileToS3 = (copyBucket, resizeKey, buffer, contentType) => s3.upload({
    Bucket: copyBucket,
    Key: resizeKey,
    Body: buffer,
    ContentType: contentType
}).promise();

const deleteFileInS3 = (originBucket, originKey) => new Promise((resolve, reject) => {
    console.log(`Running-ImageDelete-${originBucket}-${originKey}`);

    const params = {
        Bucket: originBucket,
        Key: originKey
    };

    s3.deleteObject(params, (err, data) => {
        if(err){
            console.log(err);
            reject(err);            
        }else{
            console.log(data);
            resolve(data);
        }
    });
});
  • 사용한 라이브러리: sharp, util(기본 내장)
  • 다운로드 -> 삭제 -> 리사이징 및 업로드
  • 삭제를 리사이징 전에 하지 않으면 계속 트리거되는 버그 있음