티스토리 뷰
주의 사항
AWS Lambda와 AWS API Gateway를 사용하기 전에 알아야 할 것이 있다.
- Request payload 사이즈가 제한되며 증가할 수 없다.
- API Gateway payload limit: 10MB / AWS Lambda payload limit: 6MB
- 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”로 변경
- 매핑 템플릿(클라이언트가 보낸 파라미터를 매핑하는 템플릿) 생성
- 요청을 받을 URI 정의, 람다를 등록
- 설정
- 이진 미디어 형식
- 미디어 파일을 바이트 손실없이 람다에 전달하기 위해 바이너리로 변경
- 이진 미디어 형식 추가 > 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(기본 내장)
- 다운로드 -> 삭제 -> 리사이징 및 업로드
- 삭제를 리사이징 전에 하지 않으면 계속 트리거되는 버그 있음
'[개발] Infrastructure > AWS' 카테고리의 다른 글
미 사용 AWS EC2 보안 그룹은 어떻게 확인할까? (0) | 2021.07.28 |
---|---|
boto3 라이브러리로 미 사용중인 AWS EBS를 알아보자. (0) | 2021.07.27 |
미 사용 AWS key-pairs 를 정리하는 방법 (0) | 2021.07.15 |
AWS도 로그인 시 OTP를 쓸 수 있다? AWS MFA에 대해서 알아보자 (0) | 2021.07.13 |
AWS Well-Architected Tool이란 무엇일까? (0) | 2021.06.20 |