API 게이트웨이를 사용하여 서버 없는 사진 업로드 서비스 구축 방법
42629 단어 lambdaserverlessnodeapigateway
이 문서에서는 AWS API 게이트웨이, Lambda, S3를 사용하여 이를 실현하는 방법을 보여 드리겠습니다.이벤트 관리 웹 응용 프로그램의 예시를 사용합니다. 참여자는 특정 이벤트와 관련된 사진과 제목, 설명을 로그인하고 업로드할 수 있습니다.S3를 사용하여 사진을 저장하고 API 게이트웨이 API를 사용하여 업로드 요청을 처리합니다.요구 사항은 다음과 같습니다.
구현 시나리오 고려
과거에 비무서버 기술(예를 들어 Express.js에서)을 사용하여 유사한 기능을 구축한 후에 저는 처음에 Lambda가 지원하는 API 인터페이스 포트를 사용하여 모든 것을 처리하는 방법을 연구했습니다. 인증, 권한 수여, 파일 업로드, 마지막으로 S3 위치와 메타데이터를 데이터베이스에 쓰는 것입니다.
비록 이런 방법은 효과가 있고 실행할 수 있지만 일부 한계도 있다.
S3 사전 서명 URL을 사용하여 업로드
진일보한 연구를 통해 나는 더욱 좋은 해결 방안을 발견했다. 예를 들어 uploading objects to S3 using presigned URLs 미리 업로드 권한 수여 검사를 제공할 뿐만 아니라 구조화된 메타데이터를 사용하여 사진을 미리 표시하고 업로드하는 수단이기도 하다.
다음 그림은 웹 응용 프로그램의 요청 흐름을 보여 줍니다.
주의해야 할 것은 웹 클라이언트의 측면에서 볼 때 이것은 두 가지 과정이다.
잠입하라고...
1단계: S3 bucket 만들기
나는 Serverless Framework를 사용하여 나의 모든 클라우드 자원의 배치와 배치를 관리한다.이 응용 프로그램에 대해 나는 두 개의 독립된'서비스'(또는 창고) 를 사용하여 독립적으로 배치할 수 있다.
infra
서비스: S3 bucket, CloudFront 분포, DynamoDB 테이블과 Cognito 사용자 풀 자원을 포함합니다.photos-api
서비스: API 게이트웨이와 Lambda 함수를 포함합니다.S3 태클은 다음과 같이 정의됩니다.
resources:
Resources:
PhotosBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ‘${self:custom.photosBucketName}’
AccessControl: Private
CorsConfiguration:
CorsRules:
- AllowedHeaders: [‘*’]
AllowedMethods: [‘PUT’]
AllowedOrigins: [‘*’]
CORS 설정은 여기서 매우 중요합니다. 웹 클라이언트가 서명 URL을 가져온 후에 PUT 요청을 실행할 수 없기 때문입니다.또한 CloudFront를 CDN으로 사용하여 사용자의 사진 다운로드 지연을 최소화합니다.CloudFront 릴리스 구성here을 볼 수 있습니다.단, 이것은 선택 가능한 구성 요소입니다. 클라이언트가 S3에서 사진을 직접 읽으려면 위의
AccessControl
속성을 PublicRead
로 변경할 수 있습니다.단계 2: Initiate Upload API 게이트웨이 끝 만들기
다음 단계는 새 API 경로를 추가하는 것입니다. 클라이언트 노드에서 이 경로를 호출하여 서명된 URL을 요청할 수 있습니다.이에 대한 요청은 다음과 같습니다.
POST /events/{eventId}/photos/initiate-upload
{
"title": "Keynote Speech",
"description": "Steve walking out on stage",
"contentType": "image/png"
}
응답에는 클라이언트가 이 필드를 사용하여 S3에 업로드할 수 있는 단일 s3PutObjectUrl
필드가 있는 객체가 포함됩니다.이 URL은 다음과 같습니다.https://s3.eu-west-1.amazonaws.com/eventsapp-photos-dev.sampleapps.winterwindsoftware.com/uploads/event_1234/1d80868b-b05b-4ac7-ae52-bdb2dfb9b637.png?AWSAccessKeyId=XXXXXXXXXXXXXXX&Cache-Control=max-age%3D31557600&Content-Type=image%2Fpng&Expires=1571396945&Signature=F5eRZQOgJyxSdsAS9ukeMoFGPEA%3D&x-amz-meta-contenttype=image%2Fpng&x-amz-meta-description=Steve%20walking%20out%20on%20stage&x-amz-meta-eventid=1234&x-amz-meta-photoid=1d80868b-b05b-4ac7-ae52-bdb2dfb9b637&x-amz-meta-title=Keynote%20Speech&x-amz-security-token=XXXXXXXXXX
질의 문자열에는 다음 필드가 포함되어 있습니다.x-amz-meta-XXX
- 이 필드는 initiateUpload
Lambda 함수가 설정할 메타데이터 값을 포함합니다.x-amz-security-token
- S3 인증을 위한 임시 보안 토큰이 포함되어 있음Signature
- PUT 요청이 클라이언트에 의해 변경되지 않음(예: 메타데이터 값 변경)# serverless.yml
service: eventsapp-photos-api
…
custom:
appName: eventsapp
infraStack: ${self:custom.appName}-infra-${self:provider.stage}
awsAccountId: ${cf:${self:custom.infraStack}.AWSAccountId}
apiAuthorizer:
arn: arn:aws:cognito-idp:${self:provider.region}:${self:custom.awsAccountId}:userpool/${cf:${self:custom.infraStack}.UserPoolId}
corsConfig: true
functions:
…
httpInitiateUpload:
handler: src/http/initiate-upload.handler
iamRoleStatements:
- Effect: Allow
Action:
- s3:PutObject
Resource: arn:aws:s3:::${cf:${self:custom.infraStack}.PhotosBucket}*
events:
- http:
path: events/{eventId}/photos/initiate-upload
method: post
authorizer: ${self:custom.apiAuthorizer}
cors: ${self:custom.corsConfig}
여기에서는 다음 사항에 유의해야 합니다.serverless.yml
Lambda 함수는 지정된 경로에 대한 POST 요청을 처리합니다.httpInitiateUpload
스택의 출력) 은 함수의 infra
속성에서 참조됩니다.이로써 API 게이트웨이가 HTTP 헤더에 유효한 영패의 요청을 거부authorizer
했는지 확인합니다.Authorization
속성은 이 역할로 실행되는 IAM 역할을 만듭니다.이 역할은 iamRoleStatements
S3 사진 저장소에서 작업을 수행할 수 있습니다.특히 중요한 것은 이 권한 집합은 least privilege principle을 따른다. 클라이언트에게 되돌아오는 서명 URL은 임시 방문 영패를 포함하기 때문에 이 영패는 영패 소지자가 서명 URL을 생성하는 IAM 역할의 모든 권한을 맡을 수 있다.import S3 from 'aws-sdk/clients/s3';
import uuid from 'uuid/v4';
import { InitiateEventPhotoUploadResponse, PhotoMetadata } from '@common/schemas/photos-api';
import { isValidImageContentType, getSupportedContentTypes, getFileSuffixForContentType } from '@svc-utils/image-mime-types';
import { s3 as s3Config } from '@svc-config';
import { wrap } from '@common/middleware/apigw';
import { StatusCodeError } from '@common/utils/errors';
const s3 = new S3();
export const handler = wrap(async (event) => {
// Read metadata from path/body and validate
const eventId = event.pathParameters!.eventId;
const body = JSON.parse(event.body || '{}');
const photoMetadata: PhotoMetadata = {
contentType: body.contentType,
title: body.title,
description: body.description,
};
if (!isValidImageContentType(photoMetadata.contentType)) {
throw new StatusCodeError(400, `Invalid contentType for image. Valid values are: ${getSupportedContentTypes().join(',')}`);
}
// TODO: Add any further business logic validation here (e.g. that current user has write access to eventId)
// Create the PutObjectRequest that will be embedded in the signed URL
const photoId = uuid();
const req: S3.Types.PutObjectRequest = {
Bucket: s3Config.photosBucket,
Key: `uploads/event_${eventId}/${photoId}.${getFileSuffixForContentType(photoMetadata.contentType)!}` ,
ContentType: photoMetadata.contentType,
CacheControl: 'max-age=31557600', // instructs CloudFront to cache for 1 year
// Set Metadata fields to be retrieved post-upload and stored in DynamoDB
Metadata: {
...(photoMetadata as any),
photoId,
eventId,
},
};
// Get the signed URL from S3 and return to client
const s3PutObjectUrl = await s3.getSignedUrlPromise('putObject', req);
const result: InitiateEventPhotoUploadResponse = {
photoId,
s3PutObjectUrl,
};
return {
statusCode: 201,
body: JSON.stringify(result),
};
});
PutObject
이 이곳의 주요 관심사다.PutObject 요청을 서명 URL로 정렬합니다.CORS 헤더 추가와 포획되지 않은 오류 로그 기록 등 다양한 분야의 API 문제를 처리하기 위해
s3.getSignedUrlPromise
중간부품 함수를 사용하고 있습니다.3단계: 웹 응용 프로그램에서 파일 업로드
이제 클라이언트 논리를 실현합니다.나는 매우 기본적인 (읽기: 추함)
wrap
예시 (코드 here 를 만들었다.Cognito 인증을 관리하기 위해 Amplify's Auth library React 구성 요소를 만들었습니다. 이 구성 요소는 React Dropzone 라이브러리를 사용합니다.// components/Photos/PhotoUploader.tsx
import React, { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { uploadPhoto } from '../../utils/photos-api-client';
const PhotoUploader: React.FC<{ eventId: string }> = ({ eventId }) => {
const onDrop = useCallback(async (files: File[]) => {
console.log('starting upload', { files });
const file = files[0];
try {
const uploadResult = await uploadPhoto(eventId, file, {
// should enhance this to read title and description from text input fields.
title: 'my title',
description: 'my description',
contentType: file.type,
});
console.log('upload complete!', uploadResult);
return uploadResult;
} catch (error) {
console.error('Error uploading', error);
throw error;
}
}, [eventId]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{
isDragActive
? <p>Drop the files here ...</p>
: <p>Drag and drop some files here, or click to select files</p>
}
</div>
);
};
export default PhotoUploader;
// utils/photos-api-client.ts
import { API, Auth } from 'aws-amplify';
import axios, { AxiosResponse } from 'axios';
import config from '../config';
import { PhotoMetadata, InitiateEventPhotoUploadResponse, EventPhoto } from '../../../../services/common/schemas/photos-api';
API.configure(config.amplify.API);
const API_NAME = 'PhotosAPI';
async function getHeaders(): Promise<any> {
// Set auth token headers to be passed in all API requests
const headers: any = { };
const session = await Auth.currentSession();
if (session) {
headers.Authorization = `${session.getIdToken().getJwtToken()}`;
}
return headers;
}
export async function getPhotos(eventId: string): Promise<EventPhoto[]> {
return API.get(API_NAME, `/events/${eventId}/photos`, { headers: await getHeaders() });
}
export async function uploadPhoto(
eventId: string, photoFile: any, metadata: PhotoMetadata,
): Promise<AxiosResponse> {
const initiateResult: InitiateEventPhotoUploadResponse = await API.post(
API_NAME, `/events/${eventId}/photos/initiate-upload`, { body: metadata, headers: await getHeaders() },
);
return axios.put(initiateResult.s3PutObjectUrl, photoFile, {
headers: {
'Content-Type': metadata.contentType,
},
});
}
create-react-app
파일의 PhotoUploader
함수가 관건입니다.이것은 우리가 앞에서 언급한 두 가지 과정을 실행하고 먼저 uploadPhoto
API 게이트웨이 단점을 호출한 다음에 되돌아오는 photos-api-client.ts
에 PUT 요청을 한다.S3 put 요청에 initiate-upload
헤더가 설정되어 있는지 확인하십시오. 그렇지 않으면 서명과 일치하지 않기 때문에 거부됩니다.4단계: 데이터베이스에 사진 데이터 밀어넣기
사진이 업로드된 이상 이 웹 응용 프로그램은 이벤트에 업로드된 모든 사진 (상기
s3PutObjectUrl
기능 사용) 을 표시하는 방식이 필요합니다.이 순환을 닫고 이 조회를 가능하게 하기 위해서 데이터베이스에 사진 데이터를 기록해야 합니다.우리는 두 번째 Lambda 함수
Content-Type
를 만들어서 이 점을 실현합니다. 새로운 대상이 우리의 S3 메모리 통에 추가될 때마다 이 함수는 촉발됩니다.구성에 대해 살펴보겠습니다.
# serverless.yml
service: eventsapp-photos-api
…
functions:
…
s3ProcessUploadedPhoto:
handler: src/s3/process-uploaded-photo.handler
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource: arn:aws:dynamodb:${self:provider.region}:${self:custom.awsAccountId}:table/${cf:${self:custom.infraStack}.DynamoDBTablePrefix}*
- Effect: Allow
Action:
- s3:GetObject
- s3:HeadObject
Resource: arn:aws:s3:::${cf:${self:custom.infraStack}.PhotosBucket}*
events:
- s3:
bucket: ${cf:${self:custom.infraStack}.PhotosBucket}
event: s3:ObjectCreated:*
rules:
- prefix: uploads/
existing: true
이것은 getPhotos
이벤트에 의해 촉발되며, processUploadedPhoto
최상위 폴더에 추가된 파일에만 발생합니다.s3:ObjectCreated
부분에서, 우리는 함수를 DynamoDB 테이블에 쓰고 S3 메모리 통에서 읽을 수 있습니다.이제 함수 코드를 살펴보겠습니다.
import { S3Event } from 'aws-lambda';
import S3 from 'aws-sdk/clients/s3';
import log from '@common/utils/log';
import { EventPhotoCreate } from '@common/schemas/photos-api';
import { cloudfront } from '@svc-config';
import { savePhoto } from '@svc-models/event-photos';
const s3 = new S3();
export const handler = async (event: S3Event): Promise<void> => {
const s3Record = event.Records[0].s3;
// First fetch metadata from S3
const s3Object = await s3.headObject({ Bucket: s3Record.bucket.name, Key: s3Record.object.key }).promise();
if (!s3Object.Metadata) {
// Shouldn't get here
const errorMessage = 'Cannot process photo as no metadata is set for it';
log.error(errorMessage, { s3Object, event });
throw new Error(errorMessage);
}
// S3 metadata field names are converted to lowercase, so need to map them out carefully
const photoDetails: EventPhotoCreate = {
eventId: s3Object.Metadata.eventid,
description: s3Object.Metadata.description,
title: s3Object.Metadata.title,
id: s3Object.Metadata.photoid,
contentType: s3Object.Metadata.contenttype,
// Map the S3 bucket key to a CloudFront URL to be stored in the DB
url: `https://${cloudfront.photosDistributionDomainName}/${s3Record.object.key}`,
};
// Now write to DDB
await savePhoto(photoDetails);
};
Lambda 프로세서 함수에 전달된 이벤트 대상은 트리거된 대상의 버킷 이름과 키만 포함합니다.따라서 메타데이터를 얻기 위해서는 uploads/
S3API 호출을 사용해야 합니다.필요한 메타데이터 필드를 추출한 후, 우리는 사진을 위해 CloudFront URL (환경 변수를 통해 전송된 CloudFront 릴리스 도메인 이름 사용) 을 구축하여 DynamoDB에 저장합니다.
향후 향상된 기능
업로드 흐름에 대한 잠재적인 강화는 이미지를 데이터베이스에 저장하기 전에 이미지 최적화 절차를 추가하는 것이다.이것은 Lambda 함수가
iamRoleStatements
키 접두사에서 headObject
이벤트를 탐지한 다음에 이미지 파일을 읽고 그에 상응하는 조정과 최적화를 한 다음에 새 복사본을 같은 버킷에 저장하지만 새 S3:ObjectCreated
키 접두사에 포함됩니다.그리고 이 새 접두사를 터치하기 위해 데이터베이스에 저장된 Lambda 함수 설정을 업데이트해야 합니다.💌 만약 당신이 이 글을 좋아한다면, 당신은 등록할 수 있습니다 to my weekly newsletter on building serverless apps in AWS.
최초 발표winterwindsoftware.com.
Reference
이 문제에 관하여(API 게이트웨이를 사용하여 서버 없는 사진 업로드 서비스 구축 방법), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/paulswail/how-to-build-a-serverless-photo-upload-service-with-api-gateway-40el텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)