본문 바로가기
CLOUD/AWS

[Amplifyphoto] 썸네일 만들기

by Rainbound-IT 2021. 9. 1.
반응형

이제 업로드한 사진의 크기를 축소판으로 조정하고 API에 CreatePhoto 변형을 실행하여 업로드된 사진 데이터를 적절한 앨범 및 사용자와 연결할 수 있도록 수정할 차례입니다.

 

 

AWS Lambda를 사용하면 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있습니다. 대신 실행하려는 기능 코드를 제공하고 코드 실행을 트리거하도록 하나 이상의 이벤트를 구성하기만 하면 됩니다. Lambda 함수가 실행될 때 사용한 컴퓨팅 시간에 대해서만 비용을 지불하면 AWS가 고가용성으로 코드를 확장하는 데 필요한 모든 것을 처리합니다. 

 

먼저 사진 크기 조정을 구현하고 사진을 앨범 및 소유자와 연결하는 코드를 붙여넣습니다.

 

photoalbums/amplify/backend/function/S3Triggerxxxxxxx/src/index.js

 

더보기
/* Amplify Params - DO NOT EDIT
You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var apiPhotoalbumsGraphQLAPIIdOutput = process.env.API_PHOTOALBUMS_GRAPHQLAPIIDOUTPUT
var apiPhotoalbumsGraphQLAPIEndpointOutput = process.env.API_PHOTOALBUMS_GRAPHQLAPIENDPOINTOUTPUT

Amplify Params - DO NOT EDIT */// eslint-disable-next-line

require('es6-promise').polyfill();
require('isomorphic-fetch');
const AWS = require('aws-sdk');
const S3 = new AWS.S3({ signatureVersion: 'v4' });
const AUTH_TYPE = require('aws-appsync').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
const uuidv4 = require('uuid/v4');
const gql = require('graphql-tag');

/*
Note: Sharp requires native extensions to be installed in a way that is compatible
with Amazon Linux (in order to run successfully in a Lambda execution environment).

If you're not working in Cloud9, you can follow the instructions on http://sharp.pixelplumbing.com/en/stable/install/#aws-lambda how to install the module and native dependencies.
*/
const Sharp = require('sharp');

// We'll expect these environment variables to be defined when the Lambda function is deployed
const THUMBNAIL_WIDTH = parseInt(process.env.THUMBNAIL_WIDTH || 80, 10);
const THUMBNAIL_HEIGHT = parseInt(process.env.THUMBNAIL_HEIGHT || 80, 10);

let client = null


async function storePhotoInfo(item) {
  console.log('storePhotoItem', JSON.stringify(item))
  const createPhoto = gql`
    mutation CreatePhoto(
      $input: CreatePhotoInput!
      $condition: ModelPhotoConditionInput
    ) {
      createPhoto(input: $input, condition: $condition) {
        id
        albumId
        owner
        bucket
        fullsize {
          key
          width
          height
        }
        thumbnail {
          key
          width
          height
        }
        album {
          id
          name
          owner
        }
      }
    }
  `;

  console.log('trying to createphoto with input', JSON.stringify(item))
	const result = await client.mutate({ 
      mutation: createPhoto,
      variables: { input: item },
      fetchPolicy: 'no-cache'
    })

  console.log('result', JSON.stringify(result))
  return result
  }

function thumbnailKey(keyPrefix, filename) {
	return `${keyPrefix}/resized/${filename}`;
}

function fullsizeKey(keyPrefix, filename) {
	return `${keyPrefix}/fullsize/${filename}`;
}

function makeThumbnail(photo) {
	return Sharp(photo).resize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT).toBuffer();
}

async function resize(photoBody, bucketName, key) {
  const keyPrefix = key.substr(0, key.indexOf('/upload/'))
  const originalPhotoName = key.substr(key.lastIndexOf('/') + 1)
  const originalPhotoDimensions = await Sharp(photoBody).metadata();
  
  const thumbnail = await makeThumbnail(photoBody);

	await Promise.all([
		S3.putObject({
			Body: thumbnail,
			Bucket: bucketName,
			Key: thumbnailKey(keyPrefix, originalPhotoName),
		}).promise(),

		S3.copyObject({
			Bucket: bucketName,
			CopySource: bucketName + '/' + key,
			Key: fullsizeKey(keyPrefix, originalPhotoName),
		}).promise(),
	]);

	await S3.deleteObject({
		Bucket: bucketName,
		Key: key
	}).promise();

	return {
		photoId: originalPhotoName,
		
		thumbnail: {
			key: thumbnailKey(keyPrefix, originalPhotoName),
			width: THUMBNAIL_WIDTH,
			height: THUMBNAIL_HEIGHT
		},

		fullsize: {
			key: fullsizeKey(keyPrefix, originalPhotoName),
			width: originalPhotoDimensions.width,
			height: originalPhotoDimensions.height
		}
	};
};

async function processRecord(record) {
	const bucketName = record.s3.bucket.name;
  const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));

  console.log('processRecord', JSON.stringify(record))

  if (record.eventName !== "ObjectCreated:Put") { console.log('Is not a new file'); return; }
  if (! key.includes('upload/')) { console.log('Does not look like an upload from user'); return; }

  const originalPhoto = await S3.getObject({ Bucket: bucketName, Key: key }).promise()
  
	const metadata = originalPhoto.Metadata
  console.log('metadata', JSON.stringify(metadata))
  console.log('resize')
	const sizes = await resize(originalPhoto.Body, bucketName, key);    
  console.log('sizes', JSON.stringify(sizes))
	const id = uuidv4();
	const item = {
		id: id,
		owner: metadata.owner,
		albumId: metadata.albumid,
    bucket: bucketName,
    thumbnail: {
      width: sizes.thumbnail.width,
      height: sizes.thumbnail.height, 
      key: sizes.thumbnail.key,
    },
    fullsize: {
      width: sizes.fullsize.width,
      height: sizes.fullsize.height,
      key: sizes.fullsize.key,
    }
  }

  console.log(JSON.stringify(metadata), JSON.stringify(sizes), JSON.stringify(item))
	await storePhotoInfo(item);
}


exports.handler = async (event, context, callback) => {
  console.log('Received S3 event:', JSON.stringify(event, null, 2));

  client = new AWSAppSyncClient({
    url: process.env.API_PHOTOALBUMS_GRAPHQLAPIENDPOINTOUTPUT,
    region: process.env.REGION,
    auth: {
      type: AUTH_TYPE.AWS_IAM,
      credentials: AWS.config.credentials
    },
    disableOffline: true
  });
 
	try {
		event.Records.forEach(processRecord);
		callback(null, { status: 'Photo Processed' });
	}
	catch (err) {
		console.error(err);
		callback(err);
	}
};

 

 

Lambda 함수를 구현하기 위해 방금 붙여넣은 JavaScript 코드에는 몇 가지 종속성이 있습니다. 표준 JS 방식에서는 package.json그에 따라 코드의 파일 을 업데이트해야 합니다 .

 

photoalbums/amplify/backend/function/S3Triggerxxxxxxx/src/package.json

 

{
    "name": "S3TriggerPhotoProcessor",
    "version": "1.0.0",
    "description": "The photo uploads processor",
    "main": "index.js",
    "dependencies": {
        "aws-appsync": "^3.0.2",
        "es6-promise": "^4.2.8",
        "graphql-tag": "^2.10.1",
        "isomorphic-fetch": "^2.2.1",
        "sharp": "^0.24.0",
        "uuid": "^3.3.2"
    }
}

 

이 단계가 완료되면 Lambda 함수의 코드를 실행할 준비가 되었지만 작동하기 전에 구성 방식을 몇 가지 더 변경해야 합니다. Amplify가 업로드를 처리하기 위해 S3 Trigger 함수를 처음 생성했을 때 S3 버킷의 파일에 액세스할 수 있는 Identity and Access Management 권한이 있는 AWS Lambda 함수를 생성했지만 기본적으로 API와 통신할 수는 없습니다. Amplify가 함수를 재구성하여 API와 통신할 수 있는 권한도 갖도록 합시다.

amplify update function

 

마찬가지로, AppSync GraphQL API를 처음 구성했을 때 Amazon Cognito 사용자 풀을 사용하여 요청을 인증하도록 구성하여 웹 앱 프런트 엔드에 인증한 사용자만 API와 통신할 수 있도록 했습니다. 이제 새 사진이 앨범에 업로드될 때마다 실행되는 서버 측 Lambda 함수가 있으므로 해당 함수가 API와 통신할 수 있기를 원할 것입니다. 위에서 S3 트리거 함수를 재구성했지만 Lambda 함수에 AWS의 Identity and Access Management 인증을 통해 API와 통신할 수 있는 권한이 있다는 지정만 처리했습니다. 클라이언트가 보조 인증 메커니즘으로 IAM을 통해 인증할 수 있도록 API를 구성해야 합니다. 이를 실현하기 위해,

 

amplify update api

 

 

이렇게 하면 API에 대한 보조 옵션으로 IAM 권한 부여를 활성화할 수 있지만 기본적으로 구성된 기본 권한 부여 방법인 Amazon Cognito 사용자 풀을 통해 모든 요청을 계속 인증합니다. S3 Trigger Lambda 함수가 IAM을 통해 API와 통신할 수 있도록 Amplify의 GraphQL Transform 지시문을 통해 추가 권한 부여 방법으로 IAM을 인증하도록 특정 데이터 유형 또는 queries/mutations/subscriptions을 구성할 수 있습니다.

 

photoalbums/amplify/backend/api/photoalbums/schema.graphql

 

더보기
type Album 
@model 
@auth(rules: [
  {allow: owner},
  {allow: private, provider: iam}
]) {
    id: ID!
    name: String!
    photos: [Photo] @connection(keyName: "byAlbum", fields: ["id"])
}

type Photo 
@model 
@key(name: "byAlbum", fields: ["albumId"], queryField: "listPhotosByAlbum")
@auth(rules: [
  {allow: owner},
  {allow: private, provider: iam}
]) {
    id: ID!
    albumId: ID!
    album: Album @connection(fields: ["albumId"])
    bucket: String!
    fullsize: PhotoS3Info!
    thumbnail: PhotoS3Info!
}

type PhotoS3Info {
    key: String!
    width: Int!
    height: Int!
}

input CreatePhotoInput {
	id: ID
    owner: String
	albumId: ID!
	bucket: String!
	fullsize: PhotoS3InfoInput!
	thumbnail: PhotoS3InfoInput!
}

input PhotoS3InfoInput {
	key: String!
	width: Int!
	height: Int!
}

 

위의 API와 Lambda 함수의 권한을 재구성한 후 AWS Amplify CLI는 CloudFormation 템플릿을 재생성하여 원하는 변경 사항을 적용했습니다. 그러나 잠시 후 워크샵에서 S3 트리거 Lambda 함수가 Amazon Rekognition이라는 다른 AWS 서비스를 호출할 수 있기를 원할 것입니다(사진에서 자동 레이블 감지 수행). 이제 Lambda 함수에 이 추가 권한을 부여하도록 하겠습니다.

 

이러한 편집을 손으로 수행하는 대신 터미널에서 붙여넣고 실행할 수 있는 유용한 스크립트가 있습니다.

 

더보기
# Figure out what the S3 Trigger name is
S3_TRIGGER_NAME=$(jq -r '.function | to_entries[] | .key' amplify/backend/amplify-meta.json)

# Insert another IAM policy to allow the lambda function to call rekognition:detectLabels
cat << EOF > rekognition_policy_for_s3_trigger
        "RekognitionPolicy": {
            "DependsOn": [
                "LambdaExecutionRole"
            ],
            "Type": "AWS::IAM::Policy",
            "Properties": {
                "PolicyName": "rekognition-detect-labels",
                "Roles": [{
                    "Ref": "LambdaExecutionRole"
                }],
                "PolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [{
                        "Effect": "Allow",
                        "Action": [
                            "rekognition:detectLabels"
                        ],
                        "Resource": "*"
                    }]
                }
            }
        },
EOF
TARGET_FILE="amplify/backend/function/$S3_TRIGGER_NAME/$S3_TRIGGER_NAME-cloudformation-template.json"
LINE=$(grep -n 'AmplifyResourcesPolicy' $TARGET_FILE | cut -d ":" -f 1)
{ head -n $(($LINE-1)) $TARGET_FILE; cat rekognition_policy_for_s3_trigger; tail -n +$LINE $TARGET_FILE; } > updated_s3_trigger_cf
rm $TARGET_FILE; mv updated_s3_trigger_cf $TARGET_FILE

# Rename the amplify-generated policy name in the trigger function cf template to prevent conflicts
sed -i -e "s/amplify-lambda-execution-policy/amplify-lambda-execution-policy-api/" $TARGET_FILE

# Rename the amplify-generated policy name in the s3 cf template to prevent conflicts
STORAGE_NAME=$(jq -r '.storage | to_entries[] | .key' amplify/backend/amplify-meta.json)
sed -i -e "s/amplify-lambda-execution-policy/amplify-lambda-execution-policy-storage/" amplify/backend/storage/$STORAGE_NAME/s3-cloudformation-template.json

마지막으로 이러한 업데이트를 클라우드 환경에 푸시할 준비가 되었습니다. 후우~

 

amplify push

 

변경한 사항

  • S3 버킷에 업로드되는 각 사진을 설명하는 레코드를 수신하는 Lambda 함수를 생성했습니다. 각 사진에 대해 축소판을 만든 다음 CreatePhoto 변형을 사용하여 전체 크기 및 축소판 사진에 대한 S3 경로를 API에 직접 저장합니다.
  • RekognitionDetectLabels IAM 정책을 추가하여 Amazon Rekognition의 detectLabels API를 사용할 수 있는 권한을 함수의 역할에 부여했습니다. 이 정책은 아직 사용되지 않지만 이 파일로 작업하는 동안 편의를 위해 여기에 추가할 것이므로 자동 태그 지정과 관련된 다음 섹션에 다시 올 때 추가할 필요가 없습니다. AI와 함께하는 우리의 사진.
  • 생성된 IAM 정책에 이름 충돌이 없는지 확인하기 위해 Amplify 생성 CloudFormation 템플릿에서 약간의 역할 이름 지정 문자열 수술을 수행했습니다.

 

 

AWS Amplify CLI는 CloudFormation 템플릿을 생성하여 프로젝트의 클라우드 리소스를 관리합니다. CloudFormation 템플릿은 프로젝트의 모든 인프라를 JSON 및/또는 YAML 파일 형식의 코드로 지정하기 때문에 매우 유용합니다.

Amplify 생성 CF 템플릿에 대한 모든 수동 편집이 안전한 것은 아니며 Amplify CLI가 일부 CloudFormation 템플릿에서 편집한 내용을 덮어쓸 수 있습니다. 이 워크샵에서 변경한 모든 사항은 유지되며 Amplify에서 덮어쓰지 않습니다. 왜냐하면 편집 중인 리소스를 재구성하거나 제거하는 명령을 내리지 않기 때문입니다. CLI를 사용하여 Amplify로 이미 생성한 리소스를 재구성하려고 하면 일 이 발생할 수 있습니다 .

 

 

업로드해보면 다음과 같이 사진이 짠~ 나옵니다.

반응형

'CLOUD > AWS' 카테고리의 다른 글

AWS cloud9 보안 토큰유지 하기  (0) 2021.09.06
Amplify function build 에러  (0) 2021.09.02
[Amplifyphoto] 사진 관리하기  (0) 2021.09.01
[Amplifyphoto] 앨범관리  (0) 2021.09.01
[Amplify photo] GraphQL api 생성하기  (0) 2021.09.01

댓글