diff --git a/trivia-backend/README.md b/trivia-backend/README.md index f75044c40..602fbb2f4 100644 --- a/trivia-backend/README.md +++ b/trivia-backend/README.md @@ -43,11 +43,11 @@ There are multiple options in the [infra](infra/) folder for provisioning and de 1. ECS on Fargate, deployed via CloudFormation using [ECS task set deployments](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-external.html) 1. ECS on Fargate, deployed via CloudFormation using [CodeDeploy blue-green deployments](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/blue-green.html) 1. ECS on Fargate, deployed using [CodeDeploy blue-green deployments](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-bluegreen.html) outside of CloudFormation -1. EKS on Fargate, deployed via CloudFormation using a [CloudFormation custom resource to run kubectl](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks-legacy.KubernetesResource.html) +1. EKS on Fargate, deployed via CloudFormation using a [CloudFormation custom resource to run kubectl](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks.KubernetesManifest.html) ### ECS on Fargate (rolling update deployments) -The [cdk](infra/cdk/) folder contains an example of how to model this service with the [AWS Cloud Development Kit (AWS)](https://github.com/awslabs/aws-cdk) and deploy the service with CloudFormation, using [ECS rolling update deployments](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html). +The [cdk](infra/cdk/) folder contains the example 'ecs-service' for how to model this service with the [AWS Cloud Development Kit (AWS)](https://github.com/awslabs/aws-cdk) and deploy the service with CloudFormation, using [ECS rolling update deployments](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html). To deploy the Typescript example, run the following. ``` @@ -66,9 +66,26 @@ cdk deploy --app ecs-service.js TriviaBackendProd See the 'api-service-pipeline' example in the [pipelines](../pipelines/) folder for instructions on how to continuously deploy this example with CodePipeline's [CloudFormation deploy action](https://docs.aws.amazon.com/codepipeline/latest/userguide/integrations-action-type.html#integrations-deploy-CloudFormation). +### ECS on Fargate (task set deployments) + +The [cdk](infra/cdk/) folder contains the example 'ecs-task-sets' for how to model this service with the [AWS Cloud Development Kit (AWS)](https://github.com/awslabs/aws-cdk) and deploy the service with CloudFormation, using [ECS task set deployments](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-external.html). Note that this example does not currently have a continuous deployment pipeline example in this repo. + +To deploy the Typescript example, run the following. +``` +npm install -g aws-cdk + +npm install + +npm run build + +cdk synth -o build --app 'node ecs-task-sets.js' + +cdk deploy --app ecs-task-sets.js TriviaBackendTaskSets +``` + ### ECS on Fargate (CodeDeploy blue-green deployments, outside of CloudFormation) -The [codedeploy-blue-green](infra/codedeploy-blue-green/) folder contains examples of the configuration needed to setup and execute a [blue-green deployment with CodeDeploy](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-bluegreen.html) directly: CodeDeploy appspec file, ECS task definition file, ECS service, CodeDeploy application definition, and CodeDeploy deployment group. +The [codedeploy-blue-green](infra/codedeploy-blue-green/) folder contains an example of the configuration needed to setup and execute a [blue-green deployment with CodeDeploy](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-bluegreen.html) directly: CodeDeploy appspec file, ECS task definition file, ECS service, CodeDeploy application definition, and CodeDeploy deployment group. The non-service infrastructure (load balancer, security groups, roles, etc) is modeled and provisioned with the AWS CDK and CloudFormation. A sample pre-traffic CodeDeploy hook is modeled and provisioned with CloudFormation. In this example, the ECS service is initially created outside of CloudFormation, and all future deployments are done directly with CodeDeploy outside of CloudFormation. @@ -83,7 +100,7 @@ See the 'api-service-codedeploy-pipeline' example in the [pipelines](../pipeline ### EKS on Fargate -The [cdk](infra/cdk/) folder contains examples of how to model this service with the [AWS Cloud Development Kit (AWS)](https://github.com/awslabs/aws-cdk) and deploy it to EKS on Fargate, using a [CloudFormation custom resource to run kubectl](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks-legacy.KubernetesResource.html). Note that this example does not currently have a continuous deployment pipeline example. +The [cdk](infra/cdk/) folder contains the example 'eks-service' for how to model this service with the [AWS Cloud Development Kit (AWS)](https://github.com/awslabs/aws-cdk) and deploy it to EKS on Fargate, using a [CloudFormation custom resource to run kubectl](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks-legacy.KubernetesResource.html)). Note that this example does not currently have a continuous deployment pipeline example in this repo. First, install [kubectl](https://github.com/kubernetes/kubectl) and [eksctl](https://github.com/weaveworks/eksctl). diff --git a/trivia-backend/infra/cdk/ecs-task-sets.ts b/trivia-backend/infra/cdk/ecs-task-sets.ts new file mode 100644 index 000000000..18b5b9174 --- /dev/null +++ b/trivia-backend/infra/cdk/ecs-task-sets.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env node +import { Port, SecurityGroup, Vpc } from '@aws-cdk/aws-ec2'; +import { Repository } from '@aws-cdk/aws-ecr'; +import * as ecs from '@aws-cdk/aws-ecs'; +import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, IApplicationLoadBalancerTarget, LoadBalancerTargetProps, TargetType, Protocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import cdk = require('@aws-cdk/core'); + +class TriviaBackendStack extends cdk.Stack { + constructor(parent: cdk.App, name: string, props: cdk.StackProps) { + super(parent, name, props); + + // Configuration parameters + const imageRepo = Repository.fromRepositoryName(this, 'Repo', 'reinvent-trivia-backend'); + const tag = (process.env.IMAGE_TAG) ? process.env.IMAGE_TAG : 'latest'; + const image = ecs.ContainerImage.fromEcrRepository(imageRepo, tag) + + // Look up existing network infrastructure (default VPC) + const vpc = Vpc.fromLookup(this, 'Vpc', { + isDefault: true, + }); + const subnets = vpc.publicSubnets; + const cluster = ecs.Cluster.fromClusterAttributes(this, 'Cluster', { + clusterName: 'default', + vpc, + securityGroups: [], + }); + + // Create load balancer and security group resources + const serviceSG = new SecurityGroup(this, 'ServiceSecurityGroup', { vpc }); + + const loadBalancer = new ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true, + }); + serviceSG.connections.allowFrom(loadBalancer, Port.tcp(80)); + new cdk.CfnOutput(this, 'ServiceURL', { value: 'http://' + loadBalancer.loadBalancerDnsName }); + + const listener = loadBalancer.addListener('PublicListener', { + protocol: ApplicationProtocol.HTTP, + port: 80, + open: true, + }); + const targetGroup = listener.addTargets('ECS', { + protocol: ApplicationProtocol.HTTP, + deregistrationDelay: cdk.Duration.seconds(5), + healthCheck: { + interval: cdk.Duration.seconds(5), + path: '/', + protocol: Protocol.HTTP, + healthyThresholdCount: 2, + unhealthyThresholdCount: 3, + timeout: cdk.Duration.seconds(4) + }, + targets: [ // empty to begin with, set the target type to be 'IP' + new (class EmptyIpTarget implements IApplicationLoadBalancerTarget { + attachToApplicationTargetGroup(_: ApplicationTargetGroup): LoadBalancerTargetProps { + return { targetType: TargetType.IP }; + } + })() + ], + }); + + // Create Fargate resources: task definition, service, task set, etc + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {}); + const container = taskDefinition.addContainer('web', { + image, + logging: new ecs.AwsLogDriver({ streamPrefix: 'Service' }), + }); + container.addPortMappings({ containerPort: 80 }); + + const service = new ecs.CfnService(this, 'Service', { + cluster: cluster.clusterName, + desiredCount: 2, + deploymentController: { type: ecs.DeploymentControllerType.EXTERNAL }, + }); + service.node.addDependency(targetGroup); + service.node.addDependency(listener); + + const taskSet = new ecs.CfnTaskSet(this, 'TaskSet', { + cluster: cluster.clusterName, + service: service.attrName, + scale: { unit: 'PERCENT', value: 100 }, + taskDefinition: taskDefinition.taskDefinitionArn, + launchType: ecs.LaunchType.FARGATE.toString(), + loadBalancers: [ + { + containerName: 'web', + containerPort: 80, + targetGroupArn: targetGroup.targetGroupArn, + } + ], + networkConfiguration: { + awsVpcConfiguration: { + assignPublicIp: 'ENABLED', + securityGroups: [ serviceSG.securityGroupId ], + subnets: subnets.map(subnet => subnet.subnetId), + } + }, + }); + + new ecs.CfnPrimaryTaskSet(this, 'PrimaryTaskSet', { + cluster: cluster.clusterName, + service: service.attrName, + taskSetId: taskSet.attrId, + }); + + } +} + +const app = new cdk.App(); +new TriviaBackendStack(app, 'TriviaBackendTaskSets', { + env: { account: process.env['CDK_DEFAULT_ACCOUNT'], region: 'us-east-1' } +}); +app.synth(); diff --git a/trivia-backend/infra/cfn-task-sets/README.md b/trivia-backend/infra/cfn-task-sets/README.md deleted file mode 100644 index 8be3e5784..000000000 --- a/trivia-backend/infra/cfn-task-sets/README.md +++ /dev/null @@ -1,28 +0,0 @@ -Example of using the ECS 'EXTERNAL' deployment controller and task sets in CloudFormation. - -# Create the stack - -Follow the steps in the trivia-backend README to create the ECR repo and push an image to the 'latest' tag, then: - -``` -VPC_ID=`aws ec2 describe-vpcs --region us-east-1 --filters "Name=isDefault, Values=true" --query 'Vpcs[].VpcId' --output text` - -SUBNET_IDS=`aws ec2 describe-subnets --region us-east-1 --filters "Name=vpc-id,Values=$VPC_ID","Name=default-for-az,Values=true" --query 'Subnets[].SubnetId' --output text | tr "\\t" ","` - -aws cloudformation deploy \ - --region us-east-1 \ - --stack-name reinvent-trivia-backend-task-sets \ - --template-file template.yaml \ - --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ - --parameter-overrides \ - Vpc=$VPC_ID \ - Subnets=$SUBNET_IDS \ - ImageTag=latest \ - --tags project=reinvent-trivia - -aws cloudformation describe-stacks \ - --region us-east-1 \ - --stack-name reinvent-trivia-backend-task-sets \ - --query 'Stacks[].Outputs[].OutputValue' \ - --output text -``` diff --git a/trivia-backend/infra/cfn-task-sets/template.yaml b/trivia-backend/infra/cfn-task-sets/template.yaml deleted file mode 100644 index 1fe2493aa..000000000 --- a/trivia-backend/infra/cfn-task-sets/template.yaml +++ /dev/null @@ -1,263 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Deploy a service on AWS Fargate using ECS task sets, hosted in a public subnet, and accessible via a public load balancer. - -Parameters: - Vpc: - Type: AWS::EC2::VPC::Id - Subnets: - Type: List - ImageTag: - Type: String - -Resources: - # ECS resources - TaskDefinition: - Type: AWS::ECS::TaskDefinition - Properties: - ContainerDefinitions: - - Essential: true - Image: !Sub "${AWS::AccountId}.dkr.ecr.us-east-1.${AWS::URLSuffix}/reinvent-trivia-backend:${ImageTag}" - LogConfiguration: - LogDriver: awslogs - Options: - awslogs-group: !Ref ServiceLogGroup - awslogs-stream-prefix: Service - awslogs-region: us-east-1 - Name: web - PortMappings: - - ContainerPort: 80 - Protocol: tcp - Cpu: "256" - ExecutionRoleArn: !GetAtt ServiceTaskExecutionRole.Arn - Family: !Ref AWS::StackName - Memory: "512" - NetworkMode: awsvpc - RequiresCompatibilities: - - FARGATE - TaskRoleArn: !GetAtt ServiceTaskRole.Arn - - Service: - Type: AWS::ECS::Service - Properties: - Cluster: default - DesiredCount: 2 - DeploymentController: - Type: EXTERNAL - DependsOn: - - ServiceTargetGroup - - Listener - - TaskSet: - Type: AWS::ECS::TaskSet - Properties: - Cluster: default - Service: !Ref Service - Scale: - Unit: PERCENT - Value: 100 - TaskDefinition: !Ref TaskDefinition - LaunchType: FARGATE - LoadBalancers: - - ContainerName: web - ContainerPort: 80 - TargetGroupArn: !Ref ServiceTargetGroup - NetworkConfiguration: - AwsvpcConfiguration: - AssignPublicIp: ENABLED - SecurityGroups: - - !GetAtt ServiceSecurityGroup.GroupId - Subnets: !Ref Subnets - - PrimaryTaskSet: - Type: AWS::ECS::PrimaryTaskSet - Properties: - Cluster: default - Service: !Ref Service - TaskSetId: !GetAtt TaskSet.Id - - ServiceLogGroup: - Type: AWS::Logs::LogGroup - - # Load balancer resources - LoadBalancer: - Type: AWS::ElasticLoadBalancingV2::LoadBalancer - Properties: - Scheme: internet-facing - SecurityGroups: - - !GetAtt LoadBalancerSecurityGroup.GroupId - Subnets: !Ref Subnets - Type: application - - ServiceTargetGroup: - Type: AWS::ElasticLoadBalancingV2::TargetGroup - Properties: - HealthCheckIntervalSeconds: 5 - HealthCheckPath: / - HealthCheckProtocol: HTTP - HealthyThresholdCount: 2 - UnhealthyThresholdCount: 3 - HealthCheckTimeoutSeconds: 4 - TargetGroupAttributes: - - Key: 'deregistration_delay.timeout_seconds' - Value: 5 - Port: 80 - Protocol: HTTP - TargetType: ip - VpcId: !Ref Vpc - - Listener: - Type: AWS::ElasticLoadBalancingV2::Listener - Properties: - DefaultActions: - - Type: forward - ForwardConfig: - TargetGroups: - - TargetGroupArn: !Ref ServiceTargetGroup - Weight: 100 - LoadBalancerArn: !Ref LoadBalancer - Port: 80 - Protocol: HTTP - - # Alarms - # Alarm on unhealthy hosts and HTTP 500s at the target group level - UnhealthyHostsAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: !Sub ${AWS::StackName}-UnhealthyHosts - ComparisonOperator: GreaterThanOrEqualToThreshold - EvaluationPeriods: 2 - Dimensions: - - Name: TargetGroup - Value: !GetAtt ServiceTargetGroup.TargetGroupFullName - - Name: LoadBalancer - Value: !GetAtt LoadBalancer.LoadBalancerFullName - MetricName: UnHealthyHostCount - Namespace: AWS/ApplicationELB - Period: 300 - Statistic: Average - Threshold: 1 - - Http5xxAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: !Sub ${AWS::StackName}-Http5xx - ComparisonOperator: GreaterThanOrEqualToThreshold - EvaluationPeriods: 1 - Dimensions: - - Name: TargetGroup - Value: !GetAtt ServiceTargetGroup.TargetGroupFullName - - Name: LoadBalancer - Value: !GetAtt LoadBalancer.LoadBalancerFullName - MetricName: HTTPCode_Target_5XX_Count - Namespace: AWS/ApplicationELB - Period: 300 - Statistic: Sum - Threshold: 1 - - # Security Groups: - # Allow traffic to the load balancer from the internet, - # and from the load balancer to the ECS containers. - ServiceSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: 'Security group for reInvent Trivia backend service' - SecurityGroupEgress: - - CidrIp: 0.0.0.0/0 - Description: Allow all outbound traffic by default - IpProtocol: "-1" - VpcId: !Ref Vpc - - LoadBalancerSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: 'Security group for reInvent Trivia backend load balancer' - SecurityGroupIngress: - - CidrIp: 0.0.0.0/0 - Description: Allow from anyone on production traffic port 80 - FromPort: 80 - IpProtocol: tcp - ToPort: 80 - - CidrIp: 0.0.0.0/0 - Description: Allow from anyone on test traffic ports 9000 - 9002 - FromPort: 9000 - IpProtocol: tcp - ToPort: 9002 - VpcId: !Ref Vpc - - LoadBalancerSecurityGroupToServiceSecurityGroupEgress: - Type: AWS::EC2::SecurityGroupEgress - Properties: - GroupId: !GetAtt LoadBalancerSecurityGroup.GroupId - IpProtocol: tcp - Description: Load balancer to target - DestinationSecurityGroupId: !GetAtt ServiceSecurityGroup.GroupId - FromPort: 80 - ToPort: 80 - - LoadBalancerSecurityGroupToServiceSecurityGroupIngress: - Type: AWS::EC2::SecurityGroupIngress - Properties: - IpProtocol: tcp - Description: Load balancer to target - FromPort: 80 - GroupId: !GetAtt ServiceSecurityGroup.GroupId - SourceSecurityGroupId: !GetAtt LoadBalancerSecurityGroup.GroupId - ToPort: 80 - - # Roles: - # Task role defines the policy that the ECS tasks will have, i.e. the code running in the containers. - # By default, the task role below has no permissions. - # Task execution role provides permissions to ECS to run the tasks, like pulling the image from ECR - # and pushing logs to CloudWatch Logs. - ServiceTaskRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: ecs-tasks.amazonaws.com - Version: "2012-10-17" - - ServiceTaskExecutionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: ecs-tasks.amazonaws.com - Version: "2012-10-17" - - ServiceTaskExecutionRolePolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Action: - - ecr:BatchCheckLayerAvailability - - ecr:GetDownloadUrlForLayer - - ecr:BatchGetImage - Effect: Allow - Resource: !Sub arn:aws:ecr:us-east-1:${AWS::AccountId}:repository/reinvent-trivia-backend - - Action: ecr:GetAuthorizationToken - Effect: Allow - Resource: "*" - - Action: - - logs:CreateLogStream - - logs:PutLogEvents - Effect: Allow - Resource: !GetAtt ServiceLogGroup.Arn - Version: "2012-10-17" - PolicyName: !Sub ${AWS::StackName}-ServiceTaskExecutionRolePolicy - Roles: - - !Ref ServiceTaskExecutionRole - -Outputs: - ServiceURL: - Value: !Join - - "" - - - http:// - - !GetAtt LoadBalancer.DNSName