Part 2 - Hands-on: Java Applikation and CloudFormation Templates

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

 

Docker and Java Spring-Boot

Clone the example application:

CloudFormation Templates

Clone the CloudFormation Templates inside your application root folder:

Cleaning up the directory

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

Testing and Uploading the Docker Image to ECR

Building the jar
  • mvn clean install
Building the docker image
  • mvn dockerfile:build
Running the image locally
  • docker run -p 8080:8080 -t springio/gs-spring-boot-docker
Stopping the container
  • docker ps
  • docker stop
Uploading docker image to ECR

Run the following commands in commandline:

  • aws ecr get-login --no-include-email
    This command will return a login command. Copy and paste it in the commandline and execute it.
  • docker tag springio/gs-spring-boot-docker:latest:1.0.0
  • docker push :1.0.0

 

This will be the first version of our application.

Changing the CloudFormation Templates

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.

service.yaml

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

TaskDefinition Image

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

Blue-Green Deployment

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)

 

Memory

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:

  • mvn clean install
  • mvn dockerfile:build
  • docker run -p 8080:8080 -t springio/gs-spring-boot-docker
  • docker stats

 

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.

master.yaml

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, "/" ]]

Slimmed down

The reference architecture template has resource definitions that we do not need.

The master.yaml above defines only the necessary resources.

TemplateURL

Replace the placeholder with the link to your S3 Bucket

Example:

TemplateURL: /cloudformation/infrastructure/lifecyclehook.yaml

TemplateURLhttps://s3.eu-central-1.amazonaws.com/qstutorialbucket/cloudformation/infrastructure/lifecyclehook.yaml

Add Tags

This allows our finance department to determine the costs of our project.

Tags:
     -  Key: "Organisation"
        Value: "Qualysoft"
    -   Key: "Project"
        Value: "Volluto"

Git

Connecting to CodeCommit

Steps to connect to CodeCommit:

  • Generate Git credentials for your IAM user (NOT Jenkins). Download the credentials and save them to a secure location on your computer.
  • Initialize a git repository: git init
  • Add a remote url pointing to your CodeCommit repository: git remote add origin
  • When prompted for a user name and password, provide the Git credentials you saved into the corresponding fields.

Commit

Add all files: git add .

Commit changes: git commit -m ""

Push to remote repository: git push origin master

Tag

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.