Trigger Cloudformation Blue/Green With Image Parameter Change

Viewed 511

In my Cloudformation template, I am using the blue-green deployment trigger and have the following TaskDefinition

TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  DependsOn: LogGroup
  Properties:
    Family: "task-family-name"
    NetworkMode: awsvpc
    RequiresCompatibilities: [FARGATE]
    Cpu: 512
    Memory: 1024
    ExecutionRoleArn: arn:aws:iam::579072907853:role/ecsTaskExecutionRole
    TaskRoleArn: !Ref ServiceRole
    ContainerDefinitions:
      - Name: !Ref ServiceName
        Essential: true
        Image: !Ref ImageARN
        PortMappings:
          - ContainerPort: !Ref ContainerPort
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-region: !Ref AWS::Region
            awslogs-group: !Ref LogGroup
            awslogs-stream-prefix: ecs
        Environment: 
          - Name: RACK_ENV
            Value: !Ref EnvironmentName

If I update the stack with a new value for the ImageARN parameter, I get the following error

'CodeDeployBlueGreenHook' of type AWS::CodeDeploy::BlueGreen failed with message: The submitted template's primary task definition 'TaskDefinition' was never updated. The template's 'AWS::CodeDeployBlueGreen' Transform might be missing or removed

If I don't use a parameter but instead use a hardcoded Image then update the template it works fine.

Is there a way to use a parameter?

Example

Below is a full example of a template that has this issue. I change the ImageARN parameter between these two images

nginx:1.18.0@sha256:001487e0a8c6abf91351ab45fdb308c0c2b95e8a67260b7ca5cf064462689a9e
nginx:1.19.4@sha256:aec3f367f48745b280ee2fd8d8469c0c0ec6b9b2fca3cd3e6cff03e1b69ae054
AWSTemplateFormatVersion: 2010-09-09
Parameters: 
  Image: 
    Type: String
  CIDRBlock: 
    Type: String
    Default: 192.168.5.0/24
    Description: CIDR block for VPC
  ExecutionRoleARN:
    Type: String

Transform:
  - AWS::CodeDeployBlueGreen

Hooks:
  CodeDeployBlueGreenHook:
    Type: AWS::CodeDeploy::BlueGreen
    Properties:
      TrafficRoutingConfig:
        Type: AllAtOnce
      Applications:
        - Target:
            Type: AWS::ECS::Service
            LogicalID: ECSService
          ECSAttributes:
            TaskDefinitions:
              - TaskDefinition
              - GreenTaskDefinition
            TaskSets:
              - TaskSet
              - GreenTaskSet
            TrafficRouting:
              ProdTrafficRoute:
                Type: AWS::ElasticLoadBalancingV2::Listener
                LogicalID: HTTPListener
              TargetGroups:
                - TargetGroup
                - TargetGroupGreen
    
Resources: 
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP to load balancer
      VpcId: !Ref Vpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
      - IpProtocol: -1
        CidrIp: 0.0.0.0/0

  FargateSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow port 80 to service
      VpcId: !Ref Vpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        SourceSecurityGroupId: !Ref ALBSecurityGroup
      SecurityGroupEgress:
      - IpProtocol: -1
        CidrIp: 0.0.0.0/0

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: load-balancer
      Type: application
      IpAddressType: ipv4
      Scheme: internet-facing
      SecurityGroups: [!Ref ALBSecurityGroup]
      Subnets: [!Ref Subnet1, !Ref Subnet2]

  HTTPListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 5
      Name: targetgroup-blue
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 10
      VpcId: !Ref Vpc

  TargetGroupGreen:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 5
      Name: targetgroup-green
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 10
      VpcId: !Ref Vpc
      
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: "task-family-name"
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: 512
      Memory: 1024
      ExecutionRoleArn: !Ref ExecutionRoleARN
      ContainerDefinitions:
        - Name: Container
          Essential: true
          Image: !Ref Image
          PortMappings:
            - ContainerPort: 80

  TaskSet:
    Type: AWS::ECS::TaskSet
    Properties:
      Cluster: !Ref ECSCluster
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsVpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups: [!Ref FargateSecurityGroup]
          Subnets: [!Ref Subnet1, !Ref Subnet2]
      Scale:
        Unit: PERCENT
        Value: 100
      Service: !Ref ECSService
      TaskDefinition: !Ref TaskDefinition
      PlatformVersion: LATEST
      LoadBalancers: 
        - ContainerName: Container
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup

  ECSService:
    Type: AWS::ECS::Service
    DependsOn: HTTPListener
    Properties: 
      ServiceName: service
      Cluster: !Ref ECSCluster
      DeploymentController: 
        Type: EXTERNAL
      DesiredCount: 4
      EnableECSManagedTags: true
    
  PrimaryTaskSet:
    Type: 'AWS::ECS::PrimaryTaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      Service: !Ref ECSService
      TaskSetId: !GetAtt 'TaskSet.Id'

  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CIDRBlock
      EnableDnsHostnames: true
      EnableDnsSupport: true

  Subnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Select [0, !Cidr [!Ref CIDRBlock, 2, 6 ]]
      AvailabilityZone: !Select [0, Fn::GetAZs: !Ref 'AWS::Region']

  Subnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Select [1, !Cidr [!Ref CIDRBlock, 2, 6 ]]
      AvailabilityZone: !Select [1, Fn::GetAZs: !Ref 'AWS::Region']

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  InternetGatewayVPCAssoc:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc

  RouteTableSub1Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref Subnet1

  RouteTableSub2Assoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref Subnet2

  InternetGatewayRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGateway
    Properties:
       RouteTableId: !Ref RouteTable
       DestinationCidrBlock: 0.0.0.0/0
       GatewayId: !Ref InternetGateway

  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: Cluster
      CapacityProviders:
        - FARGATE
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Weight: 1
2 Answers

I looked at your example, and what I have in my own projects. I can confirm that your example does not work as expected.

I investigated more and determined that the issue is likely to be due to creation of entire networking stack in your template. In my own projects I don't have VPC or subnets in my ecs templates, thus it all works.

So once I removed the VPC related things from your template it started to work as required. There are some other smaller changes, but they may not be necessary. I think the VPC-stuff is the main reason.

Also, please update the template by using Replace current template option, rather then Use current template. If you use the second option it will still not work. I think this is due to how macros work in CFN, and template should be replaced, rather then updated in-place. If you replace it, B/G in CodeDeploy is successful trigger and you can view it CodeDeploy console.

Here is the modified template:

AWSTemplateFormatVersion: 2010-09-09

Parameters: 

  ImageUrl: 
    Type: String
    
  VpcId:
    Type: 'AWS::EC2::VPC::Id'
    
  Subnet1:
    Type: 'AWS::EC2::Subnet::Id'
    
  Subnet2:
    Type: 'AWS::EC2::Subnet::Id'
    
  ExecutionRoleARN:
    Type: String

  DesiredCount:
    Type: Number
    Default: 2
      
    
Transform:
  - 'AWS::CodeDeployBlueGreen'

Hooks:
  CodeDeployBlueGreenHook:
    Type: 'AWS::CodeDeploy::BlueGreen'
    Properties:
      TrafficRoutingConfig:
        Type: AllAtOnce
      Applications:
        - Target:nginx:1.19.4@sha256:aec3f367f48745b280ee2fd8d8469c0c0ec6b9b2fca3cd3e6cff03e1b69ae054
            Type: 'AWS::ECS::Service'
            LogicalID: ECSService
          ECSAttributes:
            TaskDefinitions:
              - TaskDefinition
              - GreenTaskDefinition
            TaskSets:
              - TaskSet
              - GreenTaskSet
            TrafficRouting:
              ProdTrafficRoute:
                Type: 'AWS::ElasticLoadBalancingV2::Listener'
                LogicalID: HTTPListener
              TargetGroups:
                - TargetGroupnginx:1.19.4@sha256:aec3f367f48745b280ee2fd8d8469c0c0ec6b9b2fca3cd3e6cff03e1b69ae054
                - TargetGroupGreen
    
Resources: 

  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP to load balancer
      VpcId: !Ref VpcId
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
      - IpProtocol: -1
        CidrIp: 0.0.0.0/0

  FargateSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow port 80 to service
      VpcId: !Ref VpcId
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        SourceSecurityGroupId: !Ref ALBSecurityGroup
      SecurityGroupEgress:
      - IpProtocol: -1
        CidrIp: 0.0.0.0/0

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      #Name: load-balancer
      Type: application
      IpAddressType: ipv4
      Scheme: internet-facing
      SecurityGroups: 
        - !Ref ALBSecurityGroup
      Subnets: 
        - !Ref Subnet1
        - !Ref Subnet2

  HTTPListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref LoadBalannginx:1.19.4@sha256:aec3f367f48745b280ee2fd8d8469c0c0ec6b9b2fca3cd3e6cff03e1b69ae054cer
      Port: 80
      Protocol: HTTP

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 5
      #Name: targetgroup-blue
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 10
      VpcId: !Ref VpcId

  TargetGroupGreen:
    Type: nginx:1.19.4@sha256:aec3f367f48745b280ee2fd8d8469c0c0ec6b9b2fca3cd3e6cff03e1b69ae054AWS::ElasticLoadBalancingV2nginx:1.19.4@sha256:aec3f367f48745b280ee2fd8d8469c0c0ec6b9b2fca3cd3e6cff03e1b69ae054::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 5
      #Name: targetgroup-green
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 10
      VpcId: !Ref VpcId
      
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: "task-family-name"
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      Cpu: 256
      Memory: 512
      ExecutionRoleArn: !Ref ExecutionRoleARN
      ContainerDefinitions:
        - Name: Container
          Essential: true
          Image: !Ref ImageUrl
          PortMappings:
            - ContainerPort: 80
              Protocol: tcp             

  TaskSet:
    Type: AWS::ECS::TaskSet
    Properties:
      Cluster: !Ref ECSCluster
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsVpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups: 
            - !Ref FargateSecurityGroup
          Subnets: 
            - !Ref Subnet1 
            - !Ref Subnet2
      Scale:
        Unit: PERCENT
        Value: 100
      Service: !Ref ECSService
      TaskDefinition: !Ref TaskDefinition
      PlatformVersion: LATEST
      LoadBalancers: 
        - ContainerName: Container
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup

  ECSService:
    Type: AWS::ECS::Service
    DependsOn: HTTPListener
    Properties: 
      #ServiceName: service
      Cluster: !Ref ECSCluster
      DeploymentController: 
        Type: EXTERNAL
      DesiredCount: !Ref DesiredCount
      EnableECSManagedTags: true
    
  PrimaryTaskSet:
    Type: 'AWS::ECS::PrimaryTaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      Service: !Ref ECSService
      TaskSetId: !GetAtt TaskSet.Id

  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      #ClusterName: Cluster
      CapacityProviders:
        - FARGATE
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Weight: 1
          
Outputs:
    
  ALBDNSName:
    Value: !GetAtt LoadBalancer.DNSName

When I had this issue the solution was to replace the Image value within a step in the pipeline before the AWS CloudFomration step, basically "building" the file before deployment instead of passing parameters. Is not fancy but it works, I expect there are better solutions to this.

Related