API 게이트웨이를 사용하여 서버 없는 사진 업로드 서비스 구축 방법

따라서 REST API를 구축하고 있으며 웹 또는 모바일 응용 프로그램에서 파일을 업로드하는 데 대한 지원을 추가해야 합니다.데이터베이스에 있는 실체에 대해 업로드 파일에 대한 인용과 클라이언트가 제공하는 메타데이터를 추가해야 합니다.
이 문서에서는 AWS API 게이트웨이, Lambda, S3를 사용하여 이를 실현하는 방법을 보여 드리겠습니다.이벤트 관리 웹 응용 프로그램의 예시를 사용합니다. 참여자는 특정 이벤트와 관련된 사진과 제목, 설명을 로그인하고 업로드할 수 있습니다.S3를 사용하여 사진을 저장하고 API 게이트웨이 API를 사용하여 업로드 요청을 처리합니다.요구 사항은 다음과 같습니다.
  • 사용자는 응용 프로그램에 로그인하여 특정 이벤트의 사진 목록과 각 사진의 메타데이터(날짜, 제목, 설명 등)를 볼 수 있다.
  • 사용자가 이벤트에 참여하도록 등록된 경우 해당 이벤트의 사진만 업로드할 수 있습니다.
  • 인프라 시설을 모든 클라우드 자원의 코드로 사용하여 여러 환경으로 확대할 수 있다.(여기서는 AWS 콘솔을 사용하여 가변 작업을 수행하지 않음🚫🤠)
  • 구현 시나리오 고려


    과거에 비무서버 기술(예를 들어 Express.js에서)을 사용하여 유사한 기능을 구축한 후에 저는 처음에 Lambda가 지원하는 API 인터페이스 포트를 사용하여 모든 것을 처리하는 방법을 연구했습니다. 인증, 권한 수여, 파일 업로드, 마지막으로 S3 위치와 메타데이터를 데이터베이스에 쓰는 것입니다.
    비록 이런 방법은 효과가 있고 실행할 수 있지만 일부 한계도 있다.
  • 여러 부분의 파일 업로드와 테두리 상황을 관리하기 위해 Lambda에서 코드를 작성해야 합니다. 기존의 s3sdk는 이를 최적화했습니다.
  • Lambda의 가격은 지속 시간을 바탕으로 하기 때문에 비교적 큰 파일에 대한 기능은 더 긴 시간이 걸려야 완성할 수 있고 원가가 더 높다.
  • API 게이트웨이에는 payload size hard limit of 10MB 이 있습니다.이를 S3 file size limit of 5GB 과 비교합니다.
  • S3 사전 서명 URL을 사용하여 업로드


    진일보한 연구를 통해 나는 더욱 좋은 해결 방안을 발견했다. 예를 들어 uploading objects to S3 using presigned URLs 미리 업로드 권한 수여 검사를 제공할 뿐만 아니라 구조화된 메타데이터를 사용하여 사진을 미리 표시하고 업로드하는 수단이기도 하다.
    다음 그림은 웹 응용 프로그램의 요청 흐름을 보여 줍니다.

    주의해야 할 것은 웹 클라이언트의 측면에서 볼 때 이것은 두 가지 과정이다.
  • 업로드 요청을 시작하여 사진과 관련된 메타데이터(예를 들어 이벤트 ID, 제목, 설명 등)를 전송합니다.그런 다음 API에서 인증 검사를 수행하고 비즈니스 논리를 수행합니다(예: 활동에 참여한 사용자만 액세스 권한을 제한함). 마지막으로 안전한 사전 서명 URL을 생성하고 응답합니다.
  • 사전 서명된 URL을 사용하여 파일 자체를 업로드합니다.
  • 여기서 Cognito를 사용자 저장소로 사용하지만, API가 다른 인증 메커니즘을 사용한다면 사용자 정의 Lambda Authorizer 로 쉽게 대체할 수 있습니다.
    잠입하라고...

    1단계: S3 bucket 만들기


    나는 Serverless Framework를 사용하여 나의 모든 클라우드 자원의 배치와 배치를 관리한다.이 응용 프로그램에 대해 나는 두 개의 독립된'서비스'(또는 창고) 를 사용하여 독립적으로 배치할 수 있다.
  • infra 서비스: S3 bucket, CloudFront 분포, DynamoDB 테이블과 Cognito 사용자 풀 자원을 포함합니다.
  • photos-api 서비스: API 게이트웨이와 Lambda 함수를 포함합니다.
  • Github repo 에서 각 스택의 전체 구성을 볼 수 있지만 다음 사항을 다룹니다.
    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 요청을 처리합니다.
  • Cognito 사용자 풀 httpInitiateUpload 스택의 출력) 은 함수의 infra 속성에서 참조됩니다.이로써 API 게이트웨이가 HTTP 헤더에 유효한 영패의 요청을 거부authorizer했는지 확인합니다.
  • 모든 API 끝에 CORS 사용
  • 마지막으로, 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.

    좋은 웹페이지 즐겨찾기