Amazon ECS Deployment with CloudFormation

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

Procedure

Declare a set of configuration parameters that abstract out operational details, such as project name, etc. Then declare the task definition:

Prerequisites

Parameters:
  ProjectID:
    Type: String
    Default: themyscira
    Description: |
     The key that uniquely identifies a resource consumer (service, tool that requires AWS resources, etc.).
     The project ID is used as root when assembling the names of associated resources.
  Image:
    Type: String
  Tag:
    Type: String

AWS::ECS::TaskDefinition

Resources:
  ...
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Ref ProjectID
      RequiresCompatibilities: ['FARGATE']
      TaskRoleArn: !GetAtt TaskRole.Arn
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      NetworkMode: 'awsvpc'
      Memory: '4096'
      Cpu: '2048'
      ContainerDefinitions:
      - Name: !Sub '${ProjectID}-container'
        Image: !Sub ${Image}:${Tag}
        Essential: 'true'
        Memory: '4096'
        Cpu: '2048'
        PortMappings:
        - HostPort: 10002
          ContainerPort: 10002
        Environment:
        - Name: SPRING_PROFILES_ACTIVE
          Value: 'something'
        LogConfiguration:
          LogDriver: "awslogs"
          Options:
            awslogs-group: !Ref ServiceLogGroup
            awslogs-region: !Sub ${AWS::Region}
            awslogs-stream-prefix: 'task'

TaskRole and TaskExecutionRole, a service-specific ServiceLogGroup will also have to be declared, see Dependencies below.

Container Image Tag Considerations

If "latest" is used as container image tag above (as ${Tag}), I've noticed that under circumstances that yet have to be elucidated, the task does not get recycled during a CodePipeline deployment phase, as if the image change does not get detected. This happens even if the underlying image tagged as "latest" undeniably changes from the current version used by the task. The workaround is to use a unique tag ID for each image, as the one provided the CodeBuild's CODEBUILD_RESOLVED_SOURCE_VERSION. This is the essence of it:

1) Tag the image with CODEBUILD_RESOLVED_SOURCE_VERSION at build phase, and pass CODEBUILD_RESOLVED_SOURCE_VERSION to the deployment phase as part of the TemplateConfiguration.

...
phases:
  install:
    commands:
      ...
      - if [ -z "${CODEBUILD_RESOLVED_SOURCE_VERSION}" ]; then echo "required CODEBUILD_RESOLVED_SOURCE_VERSION variable not set" 1>&2; exit 1; else echo "CODEBUILD_RESOLVED_SOURCE_VERSION=${CODEBUILD_RESOLVED_SOURCE_VERSION}"; fi
      ...     
  post_build:
    commands:
      ...
      - docker tag ${ECR_REPOSITORY_URI}:latest ${ECR_REPOSITORY_URI}:${CODEBUILD_RESOLVED_SOURCE_VERSION}
      - docker push ${ECR_REPOSITORY_URI}:${CODEBUILD_RESOLVED_SOURCE_VERSION}
      ...
      - echo "{\"Parameters\":{... \"CodebuildResolvedSourceVersion\":\"${CODEBUILD_RESOLVED_SOURCE_VERSION}\"}}" > ${DEPLOYMENT_STACK_CONFIG_FILE}
artifacts:
  files:
    - ${DEPLOYMENT_STACK_CONFIG_FILE}

2) Declare as deployment stack parameter and used it in the Task definition:

...
Parameters:
  ...
  CodebuildResolvedSourceVersion:
    Type: String
    Description: >
      The identifier for the version of a build's source code. For GitHub, it is the commit ID.
...
Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ...
      ContainerDefinitions:
        - Name: !Sub ${ApplicationName}-container
          ...
          Image: !Sub ${EcrRepositoryUri}:${CodebuildResolvedSourceVersion}

AWS::ECS::Service

Resources:
  ...
  ServiceDefinition:
    Type: AWS::ECS::Service
    DependsOn:
      - LoadBalancerListener
    Properties:
      ServiceName: !Ref ProjectID
      LaunchType: FARGATE
      Cluster: 'playground'
      TaskDefinition: !Ref TaskDefinition
      DesiredCount: 1
      HealthCheckGracePeriodSeconds: 60
      LoadBalancers:
      - ContainerName: !Sub '${ProjectID}-container'
        ContainerPort: 10002
        TargetGroupArn: !Ref TargetGroup
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups:
          - !Ref ServiceSecurityGroup
          Subnets:
            - 'blue-subnet'
            - 'green-subnet'
      ServiceRegistries:
      - RegistryArn: !GetAtt ServiceDiscovery.Arn

The service depends on load balancing infrastructure.

Dependencies

TODO:

  • TaskRole
  • TaskExecutionRole
  • ServiceLogGroup
  • Load balancing infrastructure

Create a Cluster

AWS::ECS::Cluster
Resources:
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: <cluster-name>