사진 처리 함수를 만들었으니 이제는 함수를 실행하는 트리거를 설정해야합니다. 앨범에 업로드된 모든 사진을 처리해야 하기 때문에, Amplify로 생성한 S3 사용자 파일 버킷의 구성을 수정하여 이러한 변경 작업을 수행합니다.
photo-albums/amplify/backend/storage/photoalbumsstorage/s3-cloudformation-template.json 파일을 다음 내용으로 변경해주십시요.
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "S3 resource stack creation using Amplify CLI",
"Parameters": {
"bucketName": {
"Type": "String"
},
"authPolicyName": {
"Type": "String"
},
"unauthPolicyName": {
"Type": "String"
},
"authRoleName": {
"Type": "String"
},
"unauthRoleName": {
"Type": "String"
},
"s3PublicPolicy": {
"Type": "String"
},
"s3PrivatePolicy": {
"Type": "String"
},
"s3ProtectedPolicy": {
"Type": "String"
},
"s3UploadsPolicy": {
"Type": "String"
},
"s3ReadPolicy": {
"Type": "String"
},
"s3PermissionsAuthenticatedPublic": {
"Type": "String"
},
"s3PermissionsAuthenticatedProtected": {
"Type": "String"
},
"s3PermissionsAuthenticatedPrivate": {
"Type": "String"
},
"s3PermissionsAuthenticatedUploads": {
"Type": "String"
},
"s3PermissionsGuestPublic": {
"Type": "String"
},
"s3PermissionsGuestUploads": {
"Type": "String"
},
"AuthenticatedAllowList": {
"Type": "String"
},
"GuestAllowList": {
"Type": "String"
},
"selectedGuestPermissions": {
"Type": "CommaDelimitedList"
},
"selectedAuthenticatedPermissions": {
"Type": "CommaDelimitedList"
},
"env": {
"Type": "String"
}
},
"Conditions": {
"ShouldNotCreateEnvResources": {
"Fn::Equals": [
{
"Ref": "env"
},
"NONE"
]
},
"CreateAuthPublic": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "s3PermissionsAuthenticatedPublic"},
"DISALLOW"
]
}]
},
"CreateAuthProtected": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "s3PermissionsAuthenticatedProtected"},
"DISALLOW"
]
}]
},
"CreateAuthPrivate": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "s3PermissionsAuthenticatedPrivate"},
"DISALLOW"
]
}]
},
"CreateAuthUploads": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "s3PermissionsAuthenticatedUploads"},
"DISALLOW"
]
}]
},
"CreateGuestPublic": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "s3PermissionsGuestPublic"},
"DISALLOW"
]
}]
},
"CreateGuestUploads": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "s3PermissionsGuestUploads"},
"DISALLOW"
]
}]
},
"AuthReadAndList": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "AuthenticatedAllowList"},
"DISALLOW"
]
}]
},
"GuestReadAndList": {
"Fn::Not" : [{
"Fn::Equals" : [
{"Ref" : "GuestAllowList"},
"DISALLOW"
]
}]
}
},
"Resources": {
"InvokePhotoProcessorLambda": {
"Type" : "AWS::Lambda::Permission",
"Properties" : {
"Action" : "lambda:InvokeFunction",
"FunctionName" : "workshopphotoprocessor",
"Principal" : "s3.amazonaws.com",
"SourceAccount" : { "Ref": "AWS::AccountId" },
"SourceArn": {
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "bucketName"
},
"-",
{
"Ref": "env"
}
]
]
}
}
},
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"DeletionPolicy" : "Retain",
"Properties": {
"BucketName": {
"Fn::If": [
"ShouldNotCreateEnvResources",
{
"Ref": "bucketName"
},
{
"Fn::Join": [
"",
[
{
"Ref": "bucketName"
},
"-",
{
"Ref": "env"
}
]
]
}
]
},
"CorsConfiguration": {
"CorsRules": [
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"HEAD",
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"*"
],
"ExposedHeaders": [
"x-amz-server-side-encryption",
"x-amz-request-id",
"x-amz-id-2",
"ETag"
],
"Id": "S3CORSRuleId1",
"MaxAge": "3000"
}
]
},
"NotificationConfiguration": {
"LambdaConfigurations": [
{
"Function": {
"Fn::Join": [":", [
"arn:aws:lambda",
{ "Ref": "AWS::Region" },
{ "Ref": "AWS::AccountId" },
"function",
"workshopphotoprocessor"
]
]
},
"Event": "s3:ObjectCreated:Put",
"Filter": {
"S3Key": {
"Rules": [
{ "Name": "prefix", "Value": "uploads/" }
]
}
}
}
]
}
}
},
"DenyListS3BucketsAuth": {
"DependsOn": [ "S3Bucket" ],
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "DenyListS3Buckets",
"Roles": [ { "Ref": "authRoleName" } ],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [ "s3:ListBucket" ],
"Resource": ["*"]
}
]
}
}
},
"DenyListS3BucketsGuest": {
"DependsOn": [ "S3Bucket" ],
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "DenyListS3BucketsGuest",
"Roles": [ { "Ref": "unauthRoleName" } ],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [ "s3:ListBucket" ],
"Resource": ["*"]
}
]
}
}
},
"S3AuthPublicPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "CreateAuthPublic",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3PublicPolicy"
},
"Roles": [
{
"Ref": "authRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": {
"Fn::Split" : [ "," , {
"Ref": "s3PermissionsAuthenticatedPublic"
} ]
},
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/public/*"
]
]
}
]
}
]
}
}
},
"S3AuthProtectedPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "CreateAuthProtected",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3ProtectedPolicy"
},
"Roles": [
{
"Ref": "authRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": {
"Fn::Split" : [ "," , {
"Ref": "s3PermissionsAuthenticatedProtected"
} ]
},
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/protected/${cognito-identity.amazonaws.com:sub}/*"
]
]
}
]
}
]
}
}
},
"S3AuthPrivatePolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "CreateAuthPrivate",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3PrivatePolicy"
},
"Roles": [
{
"Ref": "authRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": {
"Fn::Split" : [ "," , {
"Ref": "s3PermissionsAuthenticatedPrivate"
} ]
},
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/private/${cognito-identity.amazonaws.com:sub}/*"
]
]
}
]
}
]
}
}
},
"S3AuthUploadPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "CreateAuthUploads",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3UploadsPolicy"
},
"Roles": [
{
"Ref": "authRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": {
"Fn::Split" : [ "," , {
"Ref": "s3PermissionsAuthenticatedUploads"
} ]
},
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/uploads/*"
]
]
}
]
}
]
}
}
},
"S3AuthReadPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "AuthReadAndList",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3ReadPolicy"
},
"Roles": [
{
"Ref": "authRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/protected/*"
]
]
}
]
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
}
]
]
}
],
"Condition": {
"StringLike": {
"s3:prefix": [
"public/",
"public/*",
"protected/",
"protected/*",
"private/${cognito-identity.amazonaws.com:sub}/",
"private/${cognito-identity.amazonaws.com:sub}/*"
]
}
}
}
]
}
}
},
"S3GuestPublicPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "CreateGuestPublic",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3PublicPolicy"
},
"Roles": [
{
"Ref": "unauthRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": {
"Fn::Split" : [ "," , {
"Ref": "s3PermissionsGuestPublic"
} ]
},
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/public/*"
]
]
}
]
}
]
}
}
},
"S3GuestUploadPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "CreateGuestUploads",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3UploadsPolicy"
},
"Roles": [
{
"Ref": "unauthRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": {
"Fn::Split" : [ "," , {
"Ref": "s3PermissionsGuestUploads"
} ]
},
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/uploads/*"
]
]
}
]
}
]
}
}
},
"S3GuestReadPolicy": {
"DependsOn": [
"S3Bucket"
],
"Condition": "GuestReadAndList",
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": {
"Ref": "s3ReadPolicy"
},
"Roles": [
{
"Ref": "unauthRoleName"
}
],
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
},
"/protected/*"
]
]
}
]
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "S3Bucket"
}
]
]
}
],
"Condition": {
"StringLike": {
"s3:prefix": [
"public/",
"public/*",
"protected/",
"protected/*"
]
}
}
}
]
}
}
}
},
"Outputs": {
"BucketName": {
"Value": {
"Ref": "S3Bucket"
},
"Description": "Bucket name for the S3 bucket"
},
"Region": {
"Value": {
"Ref": "AWS::Region"
}
}
}
}
photo-albums 디렉터리에서 amplify push
실행하여 저장소 구성을 갱신합니다.
완료될 때까지 기다립니다. 이 단계는 1~2분 정도 소요됩니다.
PhotoProcessor 람다 함수를 호출할 수 있도록 S3Bucket 리소스에 권한을 부여하는 InvokePhotoProcessorLambda 리소스를 추가하였습니다.
새 사진이 ‘uploads/’ 접두어로 추가되었을 때에 PhotoProcessor 람다함수를 호출하도록 버킷을 구성하게 S3Bucket 리소스에 NotificationConfiguration 속성을 추가하였습니다.
인증된 사용자가 S3 버킷 내용 목록을 보는 것을 방지하기 위해 DenyListS3Buckets 라는 IAM 정책을 추가하였습니다.
S3 스토리지의 사용자 파일의 기본 권한은 Amplify CLI에서 ‘public/’(과 다른 일부 접두어도)으로 시작하는 버킷이라면 어플리케이션에 로그인한 사용자가 버킷 내용을 나열할 수 있게 설정합니다. 이런 상호작용은 어플리케이션에서 노출하지 않으니, 어플리케이션을 찔러보는 누군가는 어플리케이션에 자격증명(credential)을 가져와서 S3 API를 직접 호출하여 모든 사진이 있는 버킷을 나열하는 시도를 할 수 있습니다.
사용자가 버킷 내용을 모두 열거할 필요는 없으니 인증된 사용자가 S3 버킷 내용을 나열 할 수 있는 기능을 명시적으로 거부하도록 IAM 정책을 역할로 추가했습니다.
이제 아무도 S3 API로 직접 호출하여 사용자가 업로드한 사진을 나열할 수 없습니다. 앨범과 사진의 ID로 UUID를 사용하기 때문에 사진을 찾기 위해 ID 패턴을 열거하는 호기심 많은 사용자들도 걱정할 필요없습니다.
이러한 변경 사항이 완료되면, 사진을 업로드할 수 있고 사진 처리 함수가 자동으로 실행되는 것을 확인할 수 있습니다. 사진을 앨범에 업로드하고 잠시 기다린 다음 페이지를 새로 고침하여 앨범이 새로 업로드된 사진을 표시하는지 확인하십시요. 사진이 표시되면 사진 처리 함수가 업로드에 의해 자동으로 시작되어 썸네일 이미지가 생성되고 AppSync API가 읽은 처리된 사진의 정보들도 DynamoDB 테이블에 모두 추가되었음을 의미합니다.
새로운 사진을 보기 위해 앨범보기를 새로 고치는 것은 좋은 사용자 경험이 아니지만, 이 워크샵에는 이미 많은 내용이 있고 다음장에도 더 많은 내용이 수록되어 있습니다. 짧게 말씀드려본다면, 다른 AppSync 구독으로 이것을 처리하는 한가지 방법은 사진 처리 람다 함수가 AppSync API에서 수정사항을 발생시키고 AlbumDetailsLoader 컴포넌트에서 해당 수정사항을 구독하는 것입니다. 그런데 AppSync API에 Amazon Cognito 사용자 풀 인증을 사용하고 있기 때문에, 람다 기능을 통해 이러한 수정사항을 일으키는 유일한 방법은 일종의 ‘시스템’ 사용자를 만들고(일반 사용자 가입 및 확인 프로세스를 통해), 사용자 자격증명을 안전하게 저장하고(AWS Secrets Manager 등으로), 수정사항을 유발하도록 람다 내부에서 AppSync API에 사용자로서 인증하는 방법입니다. 간단히 하기위해 여기서는 계속해서 앨범보기를 새로 고치는 방법으로 진행할 것입니다.