Search code examples
amazon-web-servicesaws-cloudformationaws-codepipelineaws-fargateaws-code-deploy

How to Organize ECS CodePipeline with CloudFormation


I'm trying to build out a series of deployment pipelines for our applications. Our applications are deployed in ECS Fargate. I have created the basic infrastructure using CloudFormation (ALB, main/secondary listeners, main/secondary target groups, FargateService, TaskDefinition). I also created a deployment pipeline for our lower environment ECS cluster like the following CodeBuild -> ECR(docker image)/S3(appspec.yml/taskdef.json) -> CodePipeline -> CodeDeploy ECS Blue/Green. The reason why I fronted the process with CodeBuild is to be able to build based on events to multiple branches.

I want to get my deployment pipeline set up in CloudFormation the same way. I couldn't add it to the CloudFormation template due to one of the target groups not being registered with a load balancer. I think this is the way that CodeDeploy works though. It adds and removes target groups from the ALB as part of the deployment process. Is there something I am missing here? Is there a way to build out a CloudFormation template in conjuction with CodePipeline and CodeDeploy for ECS?

Here is my template so far:

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Cluster:
    Type: String
    Default: cluster-dev
  DesiredCount:
    Type: Number
    Default: 1
  ContainerPort:
    Type: Number
    Default: 8080
  LaunchType:
    Type: String
    Default: Fargate
    AllowedValues:
      - Fargate
  MainTargetGroupName:
    Type: String
    MaxLength: 32
  CodeDeployTargetGroupName:
    Type: String
    MaxLength: 32
  SourceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup::Id'
  PrivateSubnets:
    Type: 'List<AWS::EC2::Subnet::Id>'
  PublicSubnets:
    Type: 'List<AWS::EC2::Subnet::Id>'
  ALBSecurityGroups:
    Type: 'List<AWS::EC2::SecurityGroup::Id>'
  VPC:
    Type: 'AWS::EC2::VPC::Id'
Conditions:
  Fargate: !Equals 
    - !Ref LaunchType
    - Fargate
  EC2: !Equals 
    - !Ref LaunchType
    - EC2
Resources:
  ApplicationLoadBalancer:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
        Name: "test-Application-Load-Balancer"
        Scheme: "internet-facing"
        Type: "application"
        Subnets: !Ref PublicSubnets
        SecurityGroups: !Ref ALBSecurityGroups
        IpAddressType: "ipv4"
        LoadBalancerAttributes: 
            - Key: "access_logs.s3.enabled"
              Value: "false"
            - Key: "idle_timeout.timeout_seconds"
              Value: "60"
            - Key: "deletion_protection.enabled"
              Value: "false"
            - Key: "routing.http2.enabled"
              Value: "true"
            - Key: "routing.http.drop_invalid_header_fields.enabled"
              Value: "false"
  ServiceTargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
        HealthCheckIntervalSeconds: 30
        HealthCheckPath: "/ping"
        Port: 443
        Protocol: "HTTPS"
        HealthCheckPort: "traffic-port"
        HealthCheckProtocol: "HTTPS"
        HealthCheckTimeoutSeconds: 5
        UnhealthyThresholdCount: 2
        TargetType: "ip"
        Matcher: 
            HttpCode: "200"
        HealthyThresholdCount: 5
        VpcId: !Ref VPC
        Name: !Ref MainTargetGroupName
        HealthCheckEnabled: true
        TargetGroupAttributes: 
          - Key: "stickiness.enabled"
            Value: "false"
          - Key: "deregistration_delay.timeout_seconds"
            Value: "300"
          - Key: "stickiness.type"
            Value: "lb_cookie"
          - Key: "stickiness.lb_cookie.duration_seconds"
            Value: "86400"
          - Key: "slow_start.duration_seconds"
            Value: "0"
          - Key: "load_balancing.algorithm.type"
            Value: "round_robin"
  GreenTargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
        HealthCheckIntervalSeconds: 30
        HealthCheckPath: "/ping"
        Port: 8443
        Protocol: "HTTPS"
        HealthCheckPort: "traffic-port"
        HealthCheckProtocol: "HTTPS"
        HealthCheckTimeoutSeconds: 5
        UnhealthyThresholdCount: 2
        TargetType: "ip"
        Matcher: 
            HttpCode: "200"
        HealthyThresholdCount: 5
        VpcId: !Ref VPC
        Name: !Ref CodeDeployTargetGroupName
        HealthCheckEnabled: true
        TargetGroupAttributes: 
          - Key: "stickiness.enabled"
            Value: "false"
          - Key: "deregistration_delay.timeout_seconds"
            Value: "300"
          - Key: "stickiness.type"
            Value: "lb_cookie"
          - Key: "stickiness.lb_cookie.duration_seconds"
            Value: "86400"
          - Key: "slow_start.duration_seconds"
            Value: "0"
          - Key: "load_balancing.algorithm.type"
            Value: "round_robin"
  HTTPSListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
        LoadBalancerArn: !Ref ApplicationLoadBalancer
        Port: 443
        Protocol: "HTTPS"
        SslPolicy: "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
        Certificates: 
          - CertificateArn: '*************************************'
        DefaultActions: 
          - Order: 1
            TargetGroupArn: !Ref ServiceTargetGroup
            Type: "forward"
  GreenListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
        LoadBalancerArn: !Ref ApplicationLoadBalancer
        Port: 8443
        Protocol: "HTTPS"
        SslPolicy: "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
        Certificates: 
          - CertificateArn: '************************'
        DefaultActions: 
          - Order: 1
            TargetGroupArn: !Ref GreenTargetGroup
            Type: "forward"
  CloudWatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Ref AWS::StackName
      RetentionInDays: 7  
  ECSTaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AWS::StackName}-task
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
      Policies:
        - PolicyName: ssm
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ssmmessages:CreateControlChannel
                  - ssmmessages:CreateDataChannel
                  - ssmmessages:OpenControlChannel
                  - ssmmessages:OpenDataChannel
                Resource:
                  - '*'
  ECSTaskExecRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AWS::StackName}-taskexec
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
  FargateService:
    Type: 'AWS::ECS::Service'
    Condition: Fargate
    DependsOn: 
      - HTTPSListener
      - GreenListener
    Properties:
      Cluster: !Ref Cluster
      DesiredCount: !Sub ${DesiredCount}
      TaskDefinition: !Ref TaskDefinition
      LaunchType: FARGATE
      HealthCheckGracePeriodSeconds: 300
      EnableExecuteCommand: true
      DeploymentController: 
        Type: CODE_DEPLOY
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups:
            - !Ref SourceSecurityGroup
          Subnets: !Ref PrivateSubnets
      LoadBalancers:
        - ContainerName: !Sub '${AWS::StackName}'
          ContainerPort: !Sub ${ContainerPort}
          TargetGroupArn: !Ref ServiceTargetGroup
    Metadata:
      'AWS::CloudFormation::Designer':
        id: ***********
  TaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      Family: !Sub '${AWS::StackName}'
      RequiresCompatibilities:
        - FARGATE
      Memory: 1024
      Cpu: 512
      NetworkMode: awsvpc
      TaskRoleArn: !GetAtt ECSTaskRole.Arn
      ExecutionRoleArn: !GetAtt ECSTaskExecRole.Arn
      ContainerDefinitions:
        - Name: !Sub '${AWS::StackName}'
          Image: '**********'
          EntryPoint:
            - 'java'
            - '-jar'
            - 'app-1.0.jar'
          Essential: true
          Memory: 1024
          Cpu: 512
          PortMappings:
            - ContainerPort: !Sub ${ContainerPort}
              HostPort: 8080
              Protocol: 'tcp'
          HealthCheck:
              Command: [ "CMD-SHELL", "curl -k -f https://localhost:8080/ping || exit 1" ]
              Interval: 30
              Retries: 5
              Timeout: 10
              StartPeriod: 30
          LogConfiguration:
            LogDriver: awslogs
            Options:
                awslogs-group: !Ref AWS::StackName
                awslogs-region: !Ref AWS::Region
                awslogs-stream-prefix: !Ref AWS::StackName
    Metadata:
      'AWS::CloudFormation::Designer':
        id: ********
Outputs:
  Service:
    Value: !Ref FargateService

Any help would be appreciated.

Jeff


Solution

  • The secret sauce was adding the following to the two target groups.

    DependsOn:
      - ApplicationLoadBalancer
    

    After further review, I think CloudFormation doesn't support the BlueGreen ECS deployment setup for CodePipeline. It's a bit of a stretch, but there get to be substantial errors when trying to do that. I have seen a few posts on StackOverflow and even AWS for this not being possible.