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.