# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Private API demo backend resources Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Access control Parameters: - pTrustedPrincipals - pAPIAccessList - pAllowedVPCEndpoints - pEnableWAF - Label: default: API details Parameters: - pAPIStageName - pAPIAccessLogRetention - pAPIGatewayAccountRole - Label: default: Network details (optional) Parameters: - pVPCID - pLambdaSubnetIDs - pCreateDDBEndpoint - pLambdaSubnetRouteTable ParameterLabels: pTrustedPrincipals: default: Who can assume the role for API access? pAPIAccessList: default: Who can access the API directly (without assuming the role)? pAllowedVPCEndpoints: default: VPC Endpoint IDs to whitelist pAPIStageName: default: API Stage name pAPIAccessLogRetention: default: API Access Log retention pAPIGatewayAccountRole: default: Create a Cloudwatch Logs role for API Gateway? pEnableWAF: default: Enable AWS WAF on the API? pVPCID: default: VPC ID pLambdaSubnetIDs: default: List of Lambda function VPC subnets pCreateDDBEndpoint: default: Create DDB VPC endpoint? pLambdaSubnetRouteTable: default: ID of the route table to update with DDB endpoint Parameters: pTrustedPrincipals: Description: >- For access using assume role: The list of principals (AWS accounts or ARNs) that can assume the API execution role Type: CommaDelimitedList Default: Nobody pAPIAccessList: Description: >- For direct access from the client: List of principals that will be explicitly granted access to the API via resource policy Type: String Default: 'None' AllowedPattern: '^[a-zA-Z0-9\-,_:\/]*$' pAllowedVPCEndpoints: Description: >- Comma separated whitelist of VPC endpoint IDs that will be added to the condition on the API Gateway resource policy. Type: String Default: 'None' AllowedPattern: '^[a-zA-Z0-9\-,]*$' pAPIStageName: Description: API stage name (first component of the API path). Type: String Default: Prod AllowedPattern: '^[a-zA-Z0-9\-_]+$' pAPIAccessLogRetention: Description: Number of days to retain API Gateway access logs in CloudWatch Type: Number Default: 60 # Values as per https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html AllowedValues: - 1 - 3 - 5 - 7 - 14 - 30 - 60 - 90 - 120 - 150 - 180 - 365 - 400 - 545 - 731 - 1827 - 3653 pAPIGatewayAccountRole: Description: >- Whether to create an account level role for API gateway allowing Cloudwatch Logs access. Required only if it has not already been set. Type: String Default: 'Yes' AllowedValues: - 'Yes' - 'No' pEnableWAF: Description: Whether to enable AWS WAF on the API Type: String Default: 'Yes' AllowedValues: - 'Yes' - 'No' pVPCID: Description: >- VPC ID in which to deploy the API backend Lambda function and DynamoDB endpoint. This value is optional, if you do not specify a VPC there will be no VPC configuration applied. Type: String Default: '' pLambdaSubnetIDs: Description: >- List of subnet IDs for the Lambda function ENIs. Required only if you are deploying into a VPC. Type: CommaDelimitedList Default: '' pCreateDDBEndpoint: Description: >- Set to No if there is already a DDB endpoint in your VPC. This is ignored if you are not deploying into a VPC. Type: String Default: 'Yes' AllowedValues: - 'Yes' - 'No' pLambdaSubnetRouteTable: Description: >- ID of the route table to update with the DDB endpoint route. Required only if you are deploying into a VPC and creating the DDB endpoint. Type: String Default: 'None' AllowedPattern: '^(None|[a-zA-Z0-9\-]*)$' Conditions: cHasAllowedEndpoints: !Not [!Equals [!Ref pAllowedVPCEndpoints, 'None']] cAPIAccessSpecified: !Not [!Equals [!Ref pAPIAccessList, 'None']] cRoleTrustSpecified: !Not [!Equals [!Select [0, !Ref pTrustedPrincipals], 'Nobody']] cCreateAPIGatewayAccountRole: !Equals [!Ref pAPIGatewayAccountRole, 'Yes'] cEnableWAF: !Equals [!Ref pEnableWAF, 'Yes'] cDeployToVPC: !Not [!Equals [!Ref pVPCID, '']] cCreateDDBEndpoint: !And - !Condition cDeployToVPC - !Equals [!Ref pCreateDDBEndpoint, 'Yes'] cUpdateRouteTable: !And - !Condition cDeployToVPC - !Not [!Equals [!Ref pLambdaSubnetRouteTable, 'None']] Resources: # CloudWatch Log group for API Gateway access logs rAccessLogsGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '${AWS::StackName}-APIAccessLogs' RetentionInDays: !Ref pAPIAccessLogRetention # API Gateway account role, allowing CloudWatch logs access rAPIGatewayAccountRole: Type: AWS::IAM::Role Condition: cCreateAPIGatewayAccountRole Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: apigateway.amazonaws.com Action: sts:AssumeRole rAPIGatewayAccountSettings: Type: AWS::ApiGateway::Account Condition: cCreateAPIGatewayAccountRole DependsOn: rAPI Properties: CloudWatchRoleArn: !GetAtt rAPIGatewayAccountRole.Arn # A role that will be granted full invoke access to the API. This is a simple # way to enable access to the API, but does not need to be used. # The trust policy will be set to this account, or the given ARNs specified # in the pWhoToTrust parameter. rAPIAccessRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: AWS: # Trust the local account if no other ARN is specified Fn::If: - cRoleTrustSpecified - Ref: pTrustedPrincipals - Ref: AWS::AccountId Action: sts:AssumeRole # The IAM policy attached to the above role. This needs to be separated due # to a circular dependency on the API resource which needs to be created # first. rAPIAccessPolicy: Type: AWS::IAM::Policy Properties: Roles: - !Ref rAPIAccessRole PolicyName: !Sub '${AWS::StackName}-APIAccessPolicy' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: execute-api:Invoke Resource: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${rAPI}/*" # API definition, using the Serverless Application Model # Sets up IAM auth and a resource policy which controls access based on the # defined roles and VPC endpoints. rAPI: Type: AWS::Serverless::Api DependsOn: rAPIFunction Properties: AccessLogSetting: DestinationArn: !GetAtt rAccessLogsGroup.Arn # This configures a very detailed access log which is helpful in troubleshooting. # https://aws.amazon.com/blogs/compute/troubleshooting-amazon-api-gateway-with-enhanced-observability-variables/ Format: >- { "requestId":"$context.requestId", "waf-error":"$context.waf.error", "waf-status":"$context.waf.status", "waf-latency":"$context.waf.latency", "waf-response":"$context.waf.wafResponseCode", "authenticate-error":"$context.authenticate.error", "authenticate-status":"$context.authenticate.status", "authenticate-latency":"$context.authenticate.latency", "integration-error":"$context.integration.error", "integration-status":"$context.integration.status", "integration-latency":"$context.integration.latency", "integration-requestId":"$context.integration.requestId", "integration-integrationStatus":"$context.integration.integrationStatus", "response-latency":"$context.responseLatency", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user", "arn":"$context.identity.userArn", "account":"$context.identity.accountId", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod", "resourcePath":"$context.resourcePath", "status":"$context.status", "message":"$context.error.message", "protocol":"$context.protocol", "responseLength":"$context.responseLength" } # This will add the appropriate AWS_IAM security scheme to the API spec Auth: DefaultAuthorizer: AWS_IAM InvokeRole: NONE # SAM doesn't yet support VPCEndpointIds in the EndpointConfiguration. # Once this is supported, this will create an additional DNS name to # simplify API invocation. EndpointConfiguration: PRIVATE Name: !Sub '${AWS::StackName}-API' StageName: !Ref pAPIStageName # Enable API Gateway logging and metrics collection MethodSettings: - HttpMethod: '*' ResourcePath: '/*' LoggingLevel: INFO MetricsEnabled: True # Intrinsic functions like Fn::Split and Fn::Join don't yet work in the above Auth # ResourcePolicy statement, so we need to define the resource policy using the # OpenAPI spec and the x-amazon-apigateway-policy extension. # https://github.com/aws/serverless-application-model/issues/1501 DefinitionBody: openapi: "3.0.1" info: title: "Private API Demo" version: "0.1" paths: /: get: # Set up a Lambda proxy integration. See here for documentation: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-extensions-integrations.html x-amazon-apigateway-integration: uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${rAPIFunction.Arn}/invocations" content_handling: CONVERT_TO_TEXT passthroughBehaviour: when_no_match httpMethod: POST type: aws_proxy # API Gateway resource policy definition. This policy allows access # only to specified IAM principals coming from specified VPC endpoint IDs. x-amazon-apigateway-policy: Version: '2012-10-17' Statement: # This creates a list of allowed principal ARNs by joining together # the list provided in the pAPIAccessList parameter to the ARN of # the role created within this template (rAPIAccessRole) - Effect: Allow Principal: AWS: Fn::Split: - ',' - Fn::Join: - ',' - - Fn::GetAtt: rAPIAccessRole.Arn - Fn::If: - cAPIAccessSpecified - Ref: pAPIAccessList - Ref: AWS::NoValue Action: execute-api:Invoke Resource: execute-api:/* # This section creates the allow list for VPC endpoint IDs that are # supplied through the pAllowedVPCEndpoints parameter. It is # implemented as a "Deny if VPC endpoint is not in <list>". - Fn::If: - cHasAllowedEndpoints - Effect: Deny Principal: "*" Action: execute-api:Invoke Resource: execute-api:/* Condition: StringNotEquals: aws:SourceVpce: Fn::Split: - ',' - Ref: pAllowedVPCEndpoints - Ref: AWS::NoValue # DynamoDB table rDDBTable: Type: AWS::Serverless::SimpleTable Properties: SSESpecification: SSEEnabled: true SSEType: KMS # Definition of the Lambda function invoked through the API Gateway rAPIFunction: Type: AWS::Serverless::Function Properties: Handler: api-backend.handler Runtime: python3.9 Description: Receives API requests for invocations through API Gateway MemorySize: 128 Timeout: 10 Events: GetApi: Type: Api Properties: Path: / Method: GET RestApiId: !Ref rAPI CodeUri: python-backend-lambda/ Environment: Variables: DDB_TABLE: !Ref rDDBTable VpcConfig: # VPC Config is optional Fn::If: - cDeployToVPC - SecurityGroupIds: - !GetAtt rAPIFunctionSecurityGroup.GroupId # Need to convert subnetID list to a string list SubnetIds: !Split [',', !Join [',', !Ref pLambdaSubnetIDs]] - !Ref AWS::NoValue Policies: - VPCAccessPolicy: {} - DynamoDBCrudPolicy: TableName: !Ref rDDBTable # AWS WAF deployment with the AWS managed rules rWAF: Type: AWS::WAFv2::WebACL Condition: cEnableWAF Properties: Name: !Sub '${AWS::StackName}-WAF-WebACL' Scope: REGIONAL Description: WAF WebACL with AWS Managed rules DefaultAction: Allow: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Sub '${AWS::StackName}-API' Rules: - Name: AWSManagedWAFRules Priority: 0 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Sub '${AWS::StackName}-AWSCommon' Statement: ManagedRuleGroupStatement: VendorName: AWS Name: AWSManagedRulesCommonRuleSet ExcludedRules: [] rWAFAssociation: Type: AWS::WAFv2::WebACLAssociation Condition: cEnableWAF # rAPIStage is the SAM-generated logical ID for the API stage resource DependsOn: rAPIStage Properties: ResourceArn: !Sub 'arn:aws:apigateway:${AWS::Region}::/restapis/${rAPI}/stages/${pAPIStageName}' WebACLArn: !GetAtt rWAF.Arn # OPTIONAL VPC resources # The remaining resources are deployed only if the function is being connected to a VPC # VPC Prefix list helper custom resource. Source: # This looks up the AWS managed prefix list for DynamoDB in the local region # https://github.com/awslabs/aws-cloudformation-templates/tree/master/aws/solutions/PrefixListResource rGetPLResourceFunction: Type: AWS::Serverless::Function Condition: cDeployToVPC Properties: Description: Retrieve prefix lists for use in Security Groups Runtime: python3.9 Handler: lambda_function.handler CodeUri: prefix-list-resource/.build/ Timeout: 300 Environment: Variables: Logging: Debug Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: ec2:DescribePrefixLists Resource: "*" # Execute the prefix list helper to retrieve the local DDB prefix list ID rDDBPrefixListID: Type: Custom::GetPLResource Condition: cDeployToVPC Properties: ServiceToken: !GetAtt rGetPLResourceFunction.Arn loglevel: debug PrefixListName: !Sub 'com.amazonaws.${AWS::Region}.dynamodb' # Security group for the API backend Lambda function rAPIFunctionSecurityGroup: Type: AWS::EC2::SecurityGroup Condition: cDeployToVPC Properties: Tags: - Key: Name Value: !Sub '${AWS::StackName}-APIBackend-SecurityGroup' GroupDescription: Control API backend function network access VpcId: !Ref pVPCID SecurityGroupEgress: - Description: Allow DNS outbound CidrIp: 0.0.0.0/0 IpProtocol: udp FromPort: 53 ToPort: 53 # Reference the DDB prefix list ID retrieved by the helper function - Description: Allow DynamoDB outbound, using prefix list IpProtocol: tcp FromPort: 443 ToPort: 443 DestinationPrefixListId: !GetAtt rDDBPrefixListID.PrefixListID # DynamoDB gateway endpoint rDDBEndpoint: Type: AWS::EC2::VPCEndpoint Condition: cCreateDDBEndpoint Properties: VpcEndpointType: Gateway VpcId: !Ref pVPCID ServiceName: !Sub 'com.amazonaws.${AWS::Region}.dynamodb' RouteTableIds: - Fn::If: - cUpdateRouteTable - Ref: pLambdaSubnetRouteTable - Ref: AWS::NoValue # Allow access only to our specific DDB table PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: '*' Action: - dynamodb:Batch* - dynamodb:DeleteItem - dynamodb:DescribeTable - dynamodb:GetItem - dynamodb:PutItem - dynamodb:Query - dynamodb:Scan - dynamodb:UpdateItem Resource: !GetAtt rDDBTable.Arn Outputs: APIGatewayID: Description: API Gateway ID Value: !Ref rAPI APIGatewayFQDN: Description: Fully qualified domain name of the API Gateway Value: !Sub '${rAPI}.execute-api.${AWS::Region}.amazonaws.com' APIAccessRole: Description: ARN of the access role for the function to assume Value: !GetAtt rAPIAccessRole.Arn