Now that we have configured AWS, we can start with the implementation of the application.
We will use the Spring-Boot Docker tutorial and the ECS Reference Architecture templates as a foundation.
Spring-Boot with Docker Tutorial: https://spring.io/guides/gs/spring-boot-docker/
CloudFormation Template: https://github.com/aws-samples/ecs-refarch-cloudformation
Clone the example application:
Clone the CloudFormation Templates inside your application root folder:
Cleanup the files, so that the file structure looks like this:
Structure
D:. | | Dockerfile | pom.xml |+---cloudformation | | buildspec.yml | | LICENSE | | master.yaml | | NOTICE | | README.md | | | +---infrastructure | | ecs-cluster.yaml | | lifecyclehook.yaml | | load-balancers.yaml | | security-groups.yaml | | vpc.yaml | | | +---services | | \---website-service | | service.yaml | | | \---tests | validate-templates.sh | \---src +---main | \---java | \---hello | Application.java | \---test \---java \---hello HelloWorldConfigurationTests.java
Run the following commands in commandline:
This will be the first version of our application.
We need to adapt the templates since we use different TaskDefinitions and Resources. In addition to that, the memory configuration needs to be adjusted and we make the CloudFormation templates ready for Blue Green Deployment.
Replace your service.yaml with the content below:
Description: > Trying out own Service. Changed Memory from 128MiB to 500MiB. Uses ~251MiB on local machine. Parameters: VPC: Description: The VPC that the ECS cluster is deployed to Type: AWS::EC2::VPC::Id Cluster: Description: Please provide the ECS Cluster ID that this service should run on Type: String DesiredCount: Description: How many instances of this task should we run across our cluster? Type: Number Default: 2 MaxCount: Description: Maximum number of instances of this task we can run across our cluster Type: Number Default: 3 Listener: Description: The Application Load Balancer listener to register with Type: String Path: Description: The path to register with the Application Load Balancer Type: String Default: / ECSServiceAutoScalingRoleARN: Description: The ECS service auto scaling role ARN Type: String Resources: Service: Type: AWS::ECS::Service DependsOn: ListenerRule Properties: Cluster: !Ref Cluster Role: !Ref ServiceRole DesiredCount: !Ref DesiredCount TaskDefinition: !Ref TaskDefinition LoadBalancers: - ContainerName: "website-service" ContainerPort: 8080 TargetGroupArn: !Ref TargetGroup DeploymentConfiguration: MaximumPercent: 100 MinimumHealthyPercent: 50 TaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: website-service ContainerDefinitions: - Name: website-service Essential: true Image: <URI>:1.0.0 Memory: 500 PortMappings: - ContainerPort: 8080 LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref AWS::StackName awslogs-region: !Ref AWS::Region CloudWatchLogsGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Ref AWS::StackName RetentionInDays: 365 TargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: VpcId: !Ref VPC Port: 80 Protocol: HTTP Matcher: HttpCode: 200-299 HealthCheckIntervalSeconds: 10 HealthCheckPath: / HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 ListenerRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: ListenerArn: !Ref Listener Priority: 1 Conditions: - Field: path-pattern Values: - !Ref Path Actions: - TargetGroupArn: !Ref TargetGroup Type: forward # This IAM Role grants the service access to register/unregister with the # Application Load Balancer (ALB). It is based on the default documented here: # http://docs.aws.amazon.com/AmazonECS/latest/developerguide/service_IAM_role.html ServiceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ecs-service-${AWS::StackName} Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Effect": "Allow", "Principal": { "Service": [ "ecs.amazonaws.com" ]}, "Action": [ "sts:AssumeRole" ] }] } Policies: - PolicyName: !Sub ecs-service-${AWS::StackName} PolicyDocument: { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "ec2:AuthorizeSecurityGroupIngress", "ec2:Describe*", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:Describe*", "elasticloadbalancing:RegisterInstancesWithLoadBalancer", "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:DescribeTargetGroups", "elasticloadbalancing:DescribeTargetHealth", "elasticloadbalancing:RegisterTargets" ], "Resource": "*" }] } ServiceScalableTarget: Type: "AWS::ApplicationAutoScaling::ScalableTarget" Properties: MaxCapacity: !Ref MaxCount MinCapacity: !Ref DesiredCount ResourceId: !Join - / - - service - !Ref Cluster - !GetAtt Service.Name RoleARN: !Ref ECSServiceAutoScalingRoleARN ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs ServiceScaleOutPolicy: Type : "AWS::ApplicationAutoScaling::ScalingPolicy" Properties: PolicyName: ServiceScaleOutPolicy PolicyType: StepScaling ScalingTargetId: !Ref ServiceScalableTarget StepScalingPolicyConfiguration: AdjustmentType: ChangeInCapacity Cooldown: 1800 MetricAggregationType: Average StepAdjustments: - MetricIntervalLowerBound: 0 ScalingAdjustment: 1 ServiceScaleInPolicy: Type : "AWS::ApplicationAutoScaling::ScalingPolicy" Properties: PolicyName: ServiceScaleInPolicy PolicyType: StepScaling ScalingTargetId: !Ref ServiceScalableTarget StepScalingPolicyConfiguration: AdjustmentType: ChangeInCapacity Cooldown: 1800 MetricAggregationType: Average StepAdjustments: - MetricIntervalUpperBound: 0 ScalingAdjustment: -1 CPUScaleOutAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: CPU utilization greater than 90% AlarmDescription: Alarm if cpu utilization greater than 90% of reserved cpu Namespace: AWS/ECS MetricName: CPUUtilization Dimensions: - Name: ClusterName Value: !Ref Cluster - Name: ServiceName Value: !GetAtt Service.Name Statistic: Maximum Period: '60' EvaluationPeriods: '3' Threshold: '90' ComparisonOperator: GreaterThanThreshold AlarmActions: - !Ref ServiceScaleOutPolicy CPUScaleInAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: CPU utilization less than 70% AlarmDescription: Alarm if cpu utilization greater than 70% of reserved cpu Namespace: AWS/ECS MetricName: CPUUtilization Dimensions: - Name: ClusterName Value: !Ref Cluster - Name: ServiceName Value: !GetAtt Service.Name Statistic: Maximum Period: '60' EvaluationPeriods: '10' Threshold: '70' ComparisonOperator: LessThanThreshold AlarmActions: - !Ref ServiceScaleInPolicy
Replace the URI placeholder with the link to your ECR.
Example:
Image: :1.0.0
Image: 222222222222.dkr.ecr.eu-central-1.amazonaws.com/springio/gs-spring-boot-docker:1.0.0
DesiredCount: Amount of your services tasks that should run simultaneously
MaximumPercent: Upper limit of your services tasks in RUNNING or PENDING state during a deployment, as a percentage of the desired number of tasks
MinimumHealthyPercent: Lower limit of your services tasks that MUST remain in the RUNNING state during a deployment, as a percentage of the desired number of tasks
services.yaml
DeploymentConfiguration: MaximumPercent: 100 MinimumHealthyPercent: 50
This configuration forces CloudFormation to have a maximum of 100% of DesiredCount tasks (2 tasks) running and keep at least 50% of DesiredCount tasks (1 task) running during a deployment.
Example:
Start |
new TaskDefinition |
deploy new task |
check health |
deploy new task |
|
Task #1 (version 1) |
Task #1 (version 2) |
Task #1 (version 2) |
Task #1 (version 2) |
Task #1 (version 2) |
|
Task #2 (version 1) |
Task #2 (version 1) |
Task #2 (version 1) |
Task #2 (version 1) |
Task #2 (version 2) |
The application from the tutorial uses more memory than the application previously defined in the template.
To see how much memory a docker image needs just start it locally and check its memory usage:
d:\UserFiles\bgally\Documents\Volluto\gs-spring-boot-docker>docker stats CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS d11f221c9c70 confident_wiles 0.46% 257.1MiB / 1.934GiB 12.98% 2.09kB / 469B 627kB / 0B 29
In this case, the application uses around 257 MiB of memory. To make sure our application has enough headroom we just double that amount to 500 MiB in the TaskDefinition.
Replace your master.yaml with the content below:
master.yaml
Description: > This template deploys a VPC, with a pair of public and private subnets spread across two Availabilty Zones. It deploys an Internet Gateway, with a default route on the public subnets. It deploys a pair of NAT Gateways (one in each AZ), and default routes for them in the private subnets. It then deploys a highly available ECS cluster using an AutoScaling Group, with ECS hosts distributed across multiple Availability Zones. Finally, it deploys a pair of example ECS services from containers published in Amazon EC2 Container Registry (Amazon ECR). Last Modified: 22nd September 2016 Author: Paul Maddox <pmaddox@amazon.com> Resources: VPC: Type: AWS::CloudFormation::Stack Properties: TemplateURL: <URI>/cloudformation/infrastructure/vpc.yaml Parameters: EnvironmentName: !Ref AWS::StackName VpcCIDR: 10.180.0.0/16 PublicSubnet1CIDR: 10.180.8.0/21 PublicSubnet2CIDR: 10.180.16.0/21 PrivateSubnet1CIDR: 10.180.24.0/21 PrivateSubnet2CIDR: 10.180.32.0/21 Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto" SecurityGroups: Type: AWS::CloudFormation::Stack Properties: TemplateURL: <URI>/cloudformation/infrastructure/security-groups.yaml Parameters: EnvironmentName: !Ref AWS::StackName VPC: !GetAtt VPC.Outputs.VPC Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto" ALB: Type: AWS::CloudFormation::Stack Properties: TemplateURL: <URI>/cloudformation/infrastructure/load-balancers.yaml Parameters: EnvironmentName: !Ref AWS::StackName VPC: !GetAtt VPC.Outputs.VPC Subnets: !GetAtt VPC.Outputs.PublicSubnets SecurityGroup: !GetAtt SecurityGroups.Outputs.LoadBalancerSecurityGroup Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto" ECS: Type: AWS::CloudFormation::Stack Properties: TemplateURL: <URI>/cloudformation/infrastructure/ecs-cluster.yaml Parameters: EnvironmentName: !Ref AWS::StackName InstanceType: t2.large ClusterSize: 2 VPC: !GetAtt VPC.Outputs.VPC SecurityGroup: !GetAtt SecurityGroups.Outputs.ECSHostSecurityGroup Subnets: !GetAtt VPC.Outputs.PrivateSubnets Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto" WebsiteService: Type: AWS::CloudFormation::Stack Properties: TemplateURL: <URI>/cloudformation/services/website-service/service.yaml Parameters: VPC: !GetAtt VPC.Outputs.VPC Cluster: !GetAtt ECS.Outputs.Cluster DesiredCount: 2 Listener: !GetAtt ALB.Outputs.Listener Path: / ECSServiceAutoScalingRoleARN: !GetAtt ECS.Outputs.ECSServiceAutoScalingRole Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto" LifecycleHook: Type: AWS::CloudFormation::Stack Properties: TemplateURL: <URI>/cloudformation/infrastructure/lifecyclehook.yaml Parameters: Cluster: !GetAtt ECS.Outputs.Cluster ECSAutoScalingGroupName: !GetAtt ECS.Outputs.ECSAutoScalingGroupName Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto" Outputs: WebsiteServiceUrl: Description: The URL endpoint for the website service Value: !Join ["", [ !GetAtt ALB.Outputs.LoadBalancerUrl, "/" ]]
The reference architecture template has resource definitions that we do not need.
The master.yaml above defines only the necessary resources.
Replace the placeholder with the link to your S3 Bucket
Example:
TemplateURL: /cloudformation/infrastructure/lifecyclehook.yaml
TemplateURL: https://s3.eu-central-1.amazonaws.com/qstutorialbucket/cloudformation/infrastructure/lifecyclehook.yaml
This allows our finance department to determine the costs of our project.
Tags: - Key: "Organisation" Value: "Qualysoft" - Key: "Project" Value: "Volluto"
Steps to connect to CodeCommit:
Add all files: git add .
Commit changes: git commit -m ""
Push to remote repository: git push origin master
Tag the current commit: git tag
Tag an older commit: git tag
Push a tag to remote repository: git push origin
Delete a tag from remote repository: git push origin :
Delete a tag from local repository: git tag -d
The application is now ready to be built and deployed.