# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT AWSTemplateFormatVersion: '2010-09-09' Description: Demo of an AWS Fargate cluster hosting APIs exposed through API Gateway. Parameters: EnvironmentName: Type: String Default: ecsapi-demo Description: "A friendly environment name that will be used for namespacing all cluster resources. Example: staging, qa, or production" PrivateDNSNamespaceName: Type: String Default: service Description: "The private DNS name that identifies the name that you want to use to locate your resources" MinContainersFoodstoreFoods: Type: Number Default: 3 Description: "Minimum number of ECS tasks per ECS service" MaxContainersFoodstoreFoods: Type: Number Default: 30 Description: "Maximum number of ECS tasks per ECS service" AutoScalingTargetValueFoodstoreFoods: Type: Number Default: 50 Description: "Target CPU utilizatio (%) for ECS services auto scaling" MinContainersPetstorePets: Type: Number Default: 3 Description: "Minimum number of ECS tasks per ECS service" MaxContainersPetstorePets: Type: Number Default: 30 Description: "Maximum number of ECS tasks per ECS service" AutoScalingTargetValuePetstorePets: Type: Number Default: 50 Description: "Target CPU utilizatio (%) for ECS services auto scaling" Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "Generic" Parameters: - EnvironmentName - PrivateDNSNamespaceName - Label: default: "FoodstoreFoods Service" Parameters: - MinContainersFoodstoreFoods - MaxContainersFoodstoreFoods - AutoScalingTargetValueFoodstoreFoods - Label: default: "PetstorePets Service" Parameters: - MinContainersPetstorePets - MaxContainersPetstorePets - AutoScalingTargetValuePetstorePets Mappings: SubnetConfig: VPC: CIDR: '10.0.0.0/16' PublicOne: CIDR: '10.0.0.0/24' PublicTwo: CIDR: '10.0.1.0/24' PublicThree: CIDR: '10.0.2.0/24' PrivateOne: CIDR: '10.0.100.0/24' PrivateTwo: CIDR: '10.0.101.0/24' PrivateThree: CIDR: '10.0.102.0/24' Resources: DynamoDBTableFoodstoreFoods: Type: AWS::DynamoDB::Table Properties: BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: foodId AttributeType: S KeySchema: - AttributeName: foodId KeyType: HASH DynamoDBTablePetstorePets: Type: AWS::DynamoDB::Table Properties: BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: petId AttributeType: S KeySchema: - AttributeName: petId KeyType: HASH VPC: Type: AWS::EC2::VPC Properties: EnableDnsSupport: true EnableDnsHostnames: true CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] PublicSubnetOne: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] MapPublicIpOnLaunch: true PublicSubnetTwo: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 1 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] MapPublicIpOnLaunch: true PublicSubnetThree: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 2 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PublicThree', 'CIDR'] MapPublicIpOnLaunch: true PrivateSubnetOne: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR'] PrivateSubnetTwo: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 1 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PrivateTwo', 'CIDR'] PrivateSubnetThree: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 2 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PrivateThree', 'CIDR'] InternetGateway: Type: AWS::EC2::InternetGateway GatewayAttachement: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref 'VPC' InternetGatewayId: !Ref 'InternetGateway' PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PublicRoute: Type: AWS::EC2::Route DependsOn: GatewayAttachement Properties: RouteTableId: !Ref 'PublicRouteTable' DestinationCidrBlock: '0.0.0.0/0' GatewayId: !Ref 'InternetGateway' PublicSubnetOneRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnetOne RouteTableId: !Ref PublicRouteTable PublicSubnetTwoRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnetTwo RouteTableId: !Ref PublicRouteTable PublicSubnetThreeRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnetThree RouteTableId: !Ref PublicRouteTable NatGatewayOneAttachment: Type: AWS::EC2::EIP DependsOn: GatewayAttachement Properties: Domain: vpc NatGatewayTwoAttachment: Type: AWS::EC2::EIP DependsOn: GatewayAttachement Properties: Domain: vpc NatGatewayThreeAttachment: Type: AWS::EC2::EIP DependsOn: GatewayAttachement Properties: Domain: vpc NatGatewayOne: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayOneAttachment.AllocationId SubnetId: !Ref PublicSubnetOne NatGatewayTwo: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayTwoAttachment.AllocationId SubnetId: !Ref PublicSubnetTwo NatGatewayThree: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayThreeAttachment.AllocationId SubnetId: !Ref PublicSubnetThree PrivateRouteTableOne: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PrivateRouteOne: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTableOne DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayOne PrivateRouteTableOneAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTableOne SubnetId: !Ref PrivateSubnetOne PrivateRouteTableTwo: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PrivateRouteTwo: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTableTwo DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayTwo PrivateRouteTableTwoAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTableTwo SubnetId: !Ref PrivateSubnetTwo PrivateRouteTableThree: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PrivateRouteThree: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTableThree DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayThree PrivateRouteTableThreeAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTableThree SubnetId: !Ref PrivateSubnetThree DynamoDBEndpoint: Type: AWS::EC2::VPCEndpoint Properties: PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: "*" Principal: "*" Resource: "*" RouteTableIds: - !Ref 'PrivateRouteTableOne' - !Ref 'PrivateRouteTableTwo' - !Ref 'PrivateRouteTableThree' ServiceName: !Sub com.amazonaws.${AWS::Region}.dynamodb VpcId: !Ref 'VPC' ECSCluster: Type: AWS::ECS::Cluster ContainerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the Fargate containers VpcId: !Ref 'VPC' ContainerSecurityGroupSelfIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref ContainerSecurityGroup SourceSecurityGroupId: !Ref ContainerSecurityGroup IpProtocol: tcp FromPort: 8080 ToPort: 8080 AutoScalingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole' ECSRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [ecs.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: - 'ec2:AttachNetworkInterface' - 'ec2:CreateNetworkInterface' - 'ec2:CreateNetworkInterfacePermission' - 'ec2:DeleteNetworkInterface' - 'ec2:DeleteNetworkInterfacePermission' - 'ec2:Describe*' - 'ec2:DetachNetworkInterface' - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' - 'elasticloadbalancing:DeregisterTargets' - 'elasticloadbalancing:Describe*' - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' - 'elasticloadbalancing:RegisterTargets' Resource: '*' ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [ecs-tasks.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: AmazonECSTaskExecutionRolePolicy PolicyDocument: Statement: - Effect: Allow Action: # Allow the ECS Tasks to download images from ECR - 'ecr:GetAuthorizationToken' - 'ecr:BatchCheckLayerAvailability' - 'ecr:GetDownloadUrlForLayer' - 'ecr:BatchGetImage' # Allow the ECS tasks to upload logs to CloudWatch - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: '*' PrivateDNSNamespace: Type: 'AWS::ServiceDiscovery::PrivateDnsNamespace' Properties: Vpc: !Ref VPC Name: !Ref PrivateDNSNamespaceName ServiceDiscoveryServiceFoodstoreFoods: Type: 'AWS::ServiceDiscovery::Service' Properties: DnsConfig: DnsRecords: - Type: SRV TTL: 60 NamespaceId: !Ref PrivateDNSNamespace HealthCheckCustomConfig: FailureThreshold: 1 Name: 'foods.foodstore' ServiceDiscoveryServicePetstorePets: Type: 'AWS::ServiceDiscovery::Service' Properties: DnsConfig: DnsRecords: - Type: SRV TTL: 60 NamespaceId: !Ref PrivateDNSNamespace HealthCheckCustomConfig: FailureThreshold: 1 Name: 'pets.petstore' TaskRoleFoodstoreFoods: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: dynamodb-table-access PolicyDocument: Statement: - Effect: Allow Action: - dynamodb:BatchGet* - dynamodb:DescribeStream - dynamodb:DescribeTable - dynamodb:Get* - dynamodb:Query - dynamodb:Scan - dynamodb:BatchWrite* - dynamodb:CreateTable - dynamodb:Delete* - dynamodb:Update* - dynamodb:PutItem Resource: !GetAtt DynamoDBTableFoodstoreFoods.Arn TaskRolePetstorePets: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: dynamodb-table-access PolicyDocument: Statement: - Effect: Allow Action: - dynamodb:BatchGet* - dynamodb:DescribeStream - dynamodb:DescribeTable - dynamodb:Get* - dynamodb:Query - dynamodb:Scan - dynamodb:BatchWrite* - dynamodb:CreateTable - dynamodb:Delete* - dynamodb:Update* - dynamodb:PutItem Resource: !GetAtt DynamoDBTablePetstorePets.Arn TaskDefinitionFoodstoreFoods: Type: 'AWS::ECS::TaskDefinition' Properties: TaskRoleArn: !GetAtt TaskRoleFoodstoreFoods.Arn RequiresCompatibilities: - FARGATE ContainerDefinitions: - Name: 'FoodstoreFoods' Image: 'simonepomata/ecsapi-demo-foodstore:latest' Essential: true PortMappings: - ContainerPort: 8080 Protocol: tcp Environment: - Name: DynamoDBTable Value: !Ref DynamoDBTableFoodstoreFoods NetworkMode: awsvpc Memory: '512' Cpu: '256' TaskDefinitionPetstorePets: Type: 'AWS::ECS::TaskDefinition' Properties: TaskRoleArn: !GetAtt TaskRolePetstorePets.Arn RequiresCompatibilities: - FARGATE ContainerDefinitions: - Name: 'PetstorePets' Image: 'simonepomata/ecsapi-demo-petstore:latest' Essential: true PortMappings: - ContainerPort: 8080 Protocol: tcp Environment: - Name: DynamoDBTable Value: !Ref DynamoDBTablePetstorePets NetworkMode: awsvpc Memory: '512' Cpu: '256' ServiceFoodstoreFoods: Type: AWS::ECS::Service DependsOn: - PrivateRouteOne - PrivateRouteTwo - PrivateRouteThree Properties: Cluster: !Ref ECSCluster TaskDefinition: !Ref TaskDefinitionFoodstoreFoods LaunchType: FARGATE DesiredCount: 3 ServiceRegistries: - RegistryArn: !GetAtt ServiceDiscoveryServiceFoodstoreFoods.Arn Port: 8080 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED Subnets: - !Ref PrivateSubnetOne - !Ref PrivateSubnetTwo - !Ref PrivateSubnetThree SecurityGroups: - !Ref ContainerSecurityGroup ServicePetstorePets: Type: AWS::ECS::Service DependsOn: - PrivateRouteOne - PrivateRouteTwo - PrivateRouteThree Properties: Cluster: !Ref ECSCluster TaskDefinition: !Ref TaskDefinitionPetstorePets LaunchType: FARGATE DesiredCount: 3 ServiceRegistries: - RegistryArn: !GetAtt ServiceDiscoveryServicePetstorePets.Arn Port: 8080 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED Subnets: - !Ref PrivateSubnetOne - !Ref PrivateSubnetTwo - !Ref PrivateSubnetThree SecurityGroups: - !Ref ContainerSecurityGroup AutoScalingTargetFoodstoreFoods: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref MinContainersFoodstoreFoods MaxCapacity: !Ref MaxContainersFoodstoreFoods ResourceId: !Join ['/', [service, !Ref ECSCluster, !GetAtt ServiceFoodstoreFoods.Name]] ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs RoleARN: !GetAtt AutoScalingRole.Arn AutoScalingTargetPetstorePets: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref MinContainersPetstorePets MaxCapacity: !Ref MaxContainersPetstorePets ResourceId: !Join ['/', [service, !Ref ECSCluster, !GetAtt ServicePetstorePets.Name]] ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs RoleARN: !GetAtt AutoScalingRole.Arn AutoScalingPolicyFoodstoreFoods: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Join ['', [!GetAtt ServiceFoodstoreFoods.Name, '-AutoScalingPolicy']] PolicyType: TargetTrackingScaling ScalingTargetId: !Ref AutoScalingTargetFoodstoreFoods TargetTrackingScalingPolicyConfiguration: PredefinedMetricSpecification: PredefinedMetricType: ECSServiceAverageCPUUtilization TargetValue: !Ref AutoScalingTargetValueFoodstoreFoods AutoScalingPolicyPetstorePets: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Join ['', [!GetAtt ServicePetstorePets.Name, '-AutoScalingPolicy']] PolicyType: TargetTrackingScaling ScalingTargetId: !Ref AutoScalingTargetPetstorePets TargetTrackingScalingPolicyConfiguration: PredefinedMetricSpecification: PredefinedMetricType: ECSServiceAverageCPUUtilization TargetValue: !Ref AutoScalingTargetValuePetstorePets HttpApiVPCLink: Type: AWS::ApiGatewayV2::VpcLink Properties: Name: !Ref EnvironmentName SecurityGroupIds: - !Ref ContainerSecurityGroup SubnetIds: - !Ref PrivateSubnetOne - !Ref PrivateSubnetTwo - !Ref PrivateSubnetThree UserPool: Type: AWS::Cognito::UserPool Properties: UsernameAttributes: - email AutoVerifiedAttributes: - email UserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: UserPoolId: !Ref UserPool GenerateSecret: false SupportedIdentityProviders: - COGNITO PreventUserExistenceErrors: ENABLED HttpApi: Type: AWS::ApiGatewayV2::Api Properties: Body: openapi: "3.0.1" info: title: !Ref EnvironmentName components: securitySchemes: my-authorizer: type: oauth2 flows: {} x-amazon-apigateway-authorizer: identitySource: "$request.header.Authorization" jwtConfiguration: audience: - !Ref 'UserPoolClient' issuer: !GetAtt 'UserPool.ProviderURL' type: jwt paths: /foodstore/foods/{foodId}: get: responses: default: description: "Default response for GET /foodstore/foods/{foodId}" x-amazon-apigateway-integration: payloadFormatVersion: "1.0" connectionId: !Ref HttpApiVPCLink type: "http_proxy" httpMethod: "ANY" uri: !GetAtt ServiceDiscoveryServiceFoodstoreFoods.Arn connectionType: "VPC_LINK" put: responses: default: description: "Default response for PUT /foodstore/foods/{foodId}" security: - my-authorizer: [] x-amazon-apigateway-integration: payloadFormatVersion: "1.0" connectionId: !Ref HttpApiVPCLink type: "http_proxy" httpMethod: "ANY" uri: !GetAtt ServiceDiscoveryServiceFoodstoreFoods.Arn connectionType: "VPC_LINK" /petstore/pets/{petId}: get: responses: default: description: "Default response for GET /petstore/pets/{petId}" x-amazon-apigateway-integration: payloadFormatVersion: "1.0" connectionId: !Ref HttpApiVPCLink type: "http_proxy" httpMethod: "ANY" uri: !GetAtt ServiceDiscoveryServicePetstorePets.Arn connectionType: "VPC_LINK" put: responses: default: description: "Default response for PUT /petstore/pets/{petId}" security: - my-authorizer: [] x-amazon-apigateway-integration: payloadFormatVersion: "1.0" connectionId: !Ref HttpApiVPCLink type: "http_proxy" httpMethod: "ANY" uri: !GetAtt ServiceDiscoveryServicePetstorePets.Arn connectionType: "VPC_LINK" x-amazon-apigateway-cors: # This is a Test client web app that invokes the API, used in the blog post, for demostration purposes only. # In production, make sure that you do NOT allow any untrusted origin. allowOrigins: - 'https://master.d34am23lsz3nvz.amplifyapp.com' allowHeaders: - '*' allowMethods: - 'PUT' - 'GET' x-amazon-apigateway-importexport-version: "1.0" HttpApiStage: Type: 'AWS::ApiGatewayV2::Stage' Properties: StageName: '$default' ApiId: !Ref 'HttpApi' AutoDeploy: true Outputs: APITestPage: Description: The URL of the sample web app client, used to test the sample API Value: !Join - '' - - "https://master.d34am23lsz3nvz.amplifyapp.com/?stackregion=" - !Ref 'AWS::Region' - "&stackhttpapi=" - !Ref 'HttpApi' - "&stackuserpool=" - !Ref 'UserPool' - "&stackuserpoolclient=" - !Ref 'UserPoolClient' APIInvokeURL: Description: Invoke URL for the HTTP API Value: !Sub 'https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com' APIInvokeURLFoodstoreFoods: Description: Invoke URL for the HTTP API for the service Foodstore Foods Value: !Sub 'https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/foodstore/foods/{foodId}' APIInvokeURLPetstorePets: Description: Invoke URL for the HTTP API for the service Petstore Pets Value: !Sub 'https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/petstore/pets/{petId}' APIId: Description: The ID of the HTTP API Value: !Ref 'HttpApi' ECSClusterName: Description: The name of the ECS cluster Value: !Ref 'ECSCluster' VpcId: Description: The ID of the VPC that this stack is deployed in Value: !Ref 'VPC' ContainerSecurityGroup: Description: A security group used to allow Fargate containers to receive traffic Value: !Ref 'ContainerSecurityGroup' PrivateDNSNamespace: Description: The ID of the private DNS namespace. Value: !Ref 'PrivateDNSNamespace'