AWSTemplateFormatVersion: "2010-09-09" Description: This template deploys the Engagement Meter App (uksb-1n47ktb) Transform: AWS::Serverless-2016-10-31 Globals: Function: Environment: Variables: API_GATEWAY: !Sub https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/PROD COGNITO_IDENTITY_POOL: !Ref CognitoIdentityPool COLLECTION_ID: !Ref CollectionId FROM_BUCKET: !Sub solution-builders-${AWS::Region} CREATE_CLOUDFRONT_DISTRIBUTION: !Ref CreateCloudFrontDistribution REGION: !Ref AWS::Region TO_BUCKET: !Ref WebUIBucket VERSION: '2.2' Api: Cors: AllowMethods: "'*'" AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" AllowOrigin: "'*'" Parameters: CollectionId: Description: AWS Resources are named based on the value of this parameter. You must customise this if you are launching more than one instance of the stack within the same account. Type: String Default: RekogDemo AllowedPattern : ^[a-zA-Z0-9_]*$ CreateCloudFrontDistribution: Description: Creates a CloudFront distribution for accessing the web interface of the demo. This must be enabled if S3 Block Public Access is enabled at an account level. Type: String Default: "false" AllowedValues: - "true" - "false" Conditions: WithCloudFront: !Equals [!Ref CreateCloudFrontDistribution, 'true'] Outputs: url: Value: !If - WithCloudFront - !Sub 'https://${CloudFrontDistribution.DomainName}' - !Sub 'https://${WebUIBucket.RegionalDomainName}/index.html' Description: Engagement Meter URL settingsJS: Value: !If - WithCloudFront - !Sub https://${CloudFrontDistribution.DomainName}/settings.js - !Sub https://${WebUIBucket}/settings.js Description: App settings.js file Resources: SetupRekognitionAndWebUI: Type: Custom::Setup Properties: ServiceToken: !GetAtt LambdaSetup.Arn Region: !Ref AWS::Region WebUIBucket: Type: AWS::S3::Bucket Properties: CorsConfiguration: CorsRules: - AllowedHeaders: ['*'] AllowedMethods: [GET] AllowedOrigins: ['*'] Id: !Sub RekogCorsRule${CollectionId} MaxAge: 3600 BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 WebUIBucketReadPolicy: Type: AWS::S3::BucketPolicy Condition: WithCloudFront Properties: Bucket: !Ref WebUIBucket PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Resource: !Sub arn:aws:s3:::${WebUIBucket}/* Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId CognitoIdentityPool: Type: AWS::Cognito::IdentityPool Properties: IdentityPoolName: !Sub RekogIdentityPool${CollectionId} AllowUnauthenticatedIdentities: true CognitoIdentityPoolRole: Type: AWS::Cognito::IdentityPoolRoleAttachment Properties: IdentityPoolId: !Ref CognitoIdentityPool Roles: authenticated: !GetAtt ApiGatewayInvokeRole.Arn unauthenticated: !GetAtt ApiGatewayInvokeRole.Arn FacesDynamoTable: Type: AWS::DynamoDB::Table Properties: TableName: !Sub RekogFaces${CollectionId} AttributeDefinitions: - AttributeName: CollectionId AttributeType: S - AttributeName: ExternalImageId AttributeType: S - AttributeName: MemberName AttributeType: S - AttributeName: JobTitle AttributeType: S KeySchema: - AttributeName: CollectionId KeyType: HASH - AttributeName: ExternalImageId KeyType: RANGE BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: geGSI KeySchema: - AttributeName: JobTitle KeyType: HASH - AttributeName: MemberName KeyType: RANGE Projection: NonKeyAttributes: - CollectionId - ExternalImageId ProjectionType: INCLUDE SentimentDynamoTable: Type: AWS::DynamoDB::Table Properties: TableName: !Sub RekogSentiment${CollectionId} AttributeDefinitions: - AttributeName: CollectionId AttributeType: S - AttributeName: TimeDetected AttributeType: N KeySchema: - AttributeName: CollectionId KeyType: HASH - AttributeName: TimeDetected KeyType: RANGE BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: gsiSentiment KeySchema: - AttributeName: CollectionId KeyType: HASH Projection: ProjectionType: ALL RekognitionInvokeRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonRekognitionFullAccess AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole DbReadRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole DbWriteRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayInvokeRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonAPIGatewayInvokeFullAccess AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Federated: - cognito-identity.amazonaws.com Action: sts:AssumeRoleWithWebIdentity Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref CognitoIdentityPool ForAnyValue:StringLike: "cognito-identity.amazonaws.com:amr": "unauthenticated" CustomResourceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: RekogDemo-setup PolicyDocument: Statement: - Effect: Allow Action: - s3:PutObject - s3:PutObjectAcl - s3:DeleteObject - s3:ListBucket Resource: - !Sub arn:aws:s3:::${WebUIBucket} - !Sub arn:aws:s3:::${WebUIBucket}/* - Effect: Allow Action: s3:GetObject Resource: !Sub 'arn:aws:s3:::solution-builders-${AWS::Region}/*' - Effect: Allow Action: - rekognition:CreateCollection - rekognition:DeleteCollection Resource: !Sub arn:aws:rekognition:${AWS::Region}:${AWS::AccountId}:collection/${CollectionId} - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* RestApi: Type: AWS::Serverless::Api Properties: Name: !Sub RekogDemo-${CollectionId} StageName: PROD EndpointConfiguration: REGIONAL DefinitionBody: swagger: 2.0 info: version: 1.0 title: !Sub RekogDemo-${CollectionId} basePath: /PROD paths: /engagement: get: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt DbReadRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:dynamodb:action/Query responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" responseTemplates: application/json: | #set($inputRoot = $input.path('$')) { "results": [ #foreach($elem in $inputRoot.Items) { "angry": "$elem.Angry.S", "calm": "$elem.Calm.S", "happy": "$elem.Happy.S", "sad": "$elem.Sad.S", "surprised": "$elem.Surprised.S" }#if($foreach.hasNext),#end #end ] } requestTemplates: application/json: !Sub | { "TableName": "${SentimentDynamoTable}", "IndexName": "gsiSentiment", "KeyConditionExpression": "CollectionId = :cid", "FilterExpression": "TimeDetected >= :td", "ProjectionExpression": "Angry,Calm,Happy,Sad,Surprised", "ExpressionAttributeValues": { ":cid": { "S": "${CollectionId}" }, ":td": { "N": "$input.params().querystring.get('timeDetected')" } } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] post: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt DbWriteRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:dynamodb:action/PutItem responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" responseTemplates: application/json: | { "ok": true } requestTemplates: application/json: !Sub | { "TableName": "${SentimentDynamoTable}", "Item": { "CollectionId": { "S": "${CollectionId}" }, "TimeDetected": { "N": "$input.json('$.timeDetected')" }, "Angry": { "S": "$input.json('$.angry')" }, "Calm": { "S": "$input.json('$.calm')" }, "Happy": { "S": "$input.json('$.happy')" }, "Sad": { "S": "$input.json('$.sad')" }, "Surprised": { "S": "$input.json('$.surprised')" } } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] /faces/add: post: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt RekognitionInvokeRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:rekognition:path// responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" requestParameters: integration.request.header.X-Amz-Target: "'RekognitionService.IndexFaces'" integration.request.header.Content-Type: "'application/x-amz-json-1.1'" requestTemplates: application/json: !Sub | { "CollectionId": "${CollectionId}", "ExternalImageId": $input.json('$.externalImageId'), "Image": { "Bytes": $input.json('$.image') } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] /faces/detect: post: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt RekognitionInvokeRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:rekognition:path// responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" requestParameters: integration.request.header.X-Amz-Target: "'RekognitionService.DetectFaces'" integration.request.header.Content-Type: "'application/x-amz-json-1.1'" requestTemplates: application/json: | { "Attributes": ["ALL"], "Image": { "Bytes": $input.json('$.image') } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] /faces/search: post: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt RekognitionInvokeRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:rekognition:path// responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" requestParameters: integration.request.header.X-Amz-Target: "'RekognitionService.SearchFacesByImage'" integration.request.header.Content-Type: "'application/x-amz-json-1.1'" requestTemplates: application/json: !Sub | { "CollectionId": "${CollectionId}", "FaceMatchThreshold": 85, "MaxFaces": 5, "Image": { "Bytes": $input.json('$.image') } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] /people: get: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt DbReadRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:dynamodb:action/Query responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" responseTemplates: application/json: | #set($inputRoot = $input.path('$')) { "people": [ #foreach($elem in $inputRoot.Items) { "externalImageId": "$elem.ExternalImageId.S", "memberName": "$elem.MemberName.S", "jobTitle": "$elem.JobTitle.S" }#if($foreach.hasNext),#end #end ] } requestTemplates: application/json: !Sub | { "TableName": "${FacesDynamoTable}", "KeyConditionExpression": "CollectionId = :cid", "ProjectionExpression": "MemberName,JobTitle,ExternalImageId", "ExpressionAttributeValues": { ":cid": { "S": "${CollectionId}" } } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] post: consumes: - application/json produces: - application/json responses: "200": description: 200 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "400": description: 400 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string "500": description: 500 response headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: credentials: !GetAtt DbWriteRole.Arn uri: !Sub arn:aws:apigateway:${AWS::Region}:dynamodb:action/PutItem responses: 4\d{2}: statusCode: "400" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 5\d{2}: statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" "200": statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" method.response.header.Access-Control-Allow-Headers: "'Content-Type'" responseTemplates: application/json: | { "ok": true } requestTemplates: application/json: !Sub | { "TableName": "${FacesDynamoTable}", "Item": { "CollectionId": { "S": "${CollectionId}" }, "ExternalImageId": { "S": $input.json('$.externalImageId') }, "JobTitle": { "S": $input.json('$.jobTitle') }, "MemberName": { "S": $input.json('$.memberName') } } } passthroughBehavior: when_no_match httpMethod: POST type: aws security: - sigv4: [] securityDefinitions: sigv4: type: apiKey name: Authorization in: header x-amazon-apigateway-authtype: awsSigv4 LambdaSetup: Type: AWS::Serverless::Function Properties: FunctionName: !Sub RekogDemoSetup${CollectionId} Runtime: nodejs16.x Handler: index.handler CodeUri: ../backend/functions/setup/ Description: !Sub Custom Lambda resource for the ${CollectionId} Cloudformation Stack MemorySize: 128 Timeout: 30 Role: !GetAtt CustomResourceRole.Arn CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: WithCloudFront Properties: CloudFrontOriginAccessIdentityConfig: Comment: !Ref WebUIBucket CloudFrontDistribution: Type: AWS::CloudFront::Distribution Condition: WithCloudFront Properties: DistributionConfig: Origins: - DomainName: !GetAtt WebUIBucket.RegionalDomainName Id: !Sub myS3Origin-${CollectionId} S3OriginConfig: OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity} Enabled: true HttpVersion: http2 Comment: The Distribution for the Rekognition Meter Web UI DefaultRootObject: index.html DefaultCacheBehavior: AllowedMethods: - HEAD - GET - OPTIONS TargetOriginId: !Sub myS3Origin-${CollectionId} ForwardedValues: QueryString: false Cookies: Forward: none ViewerProtocolPolicy: redirect-to-https PriceClass: PriceClass_All ViewerCertificate: CloudFrontDefaultCertificate: true