1
votes

I have a CloudFormation template that has two conditions defined. If we're launching the stack in us-west-2 (where the Network ELB IS supported), then it sets CreateNetworkLoadBalancer to True. If we're running this stack in sa-east-1 (Sao Paulo, where only the Classic ELB is supported), then CreateNetworkLoadBalancer is set to False, and CreateClassicLoadBalancer is True. (Example below)

Conditions:
  CreateClassicLoadBalancer: !Equals [ !Ref "AWS::Region", sa-east-1 ]
  CreateNetworkLoadBalancer: !Equals [ !Ref "AWS::Region", us-west-2 ]

The stack later defines two resources, a Network and a Classic ELB, as shown below. It spins up only the the appropriate LB, based on the region and the conditions. This all works as expected. (Example below)

Resources:
  ######################################################
  # Network Load Balancer, Target Group, etc.
  # NLB
  NLB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Condition: CreateNetworkLoadBalancer
    Properties:
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: False
      Name: !Join [ "-", [ !Ref awsTagsNamePrefix, "nlb" ] ]
      Scheme: internet-facing
      Subnets: !Ref bastionSubnetList
      Tags:
      - Key: Environment
        Value: !Ref awsTagsEnvironment
      - Key: Application
        Value: !Ref awsTagsApplication
      - Key: Name
        Value: !Join [ "-", [ !Ref awsTagsNamePrefix, "nlb" ] ]
      Type: network
      IpAddressType: ipv4
  # target group
  NLBTargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Condition: CreateNetworkLoadBalancer
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: 22
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 3
      Name: !Join [ "-", [ !Ref awsTagsNamePrefix, "elb-target-group" ] ]
      Port: 22
      Protocol: TCP
      Tags:
      - Key: Environment
        Value: !Ref awsTagsEnvironment
      - Key: Application
        Value: !Ref awsTagsApplication
      - Key: Name
        Value: !Join [ "-", [ !Ref awsTagsNamePrefix, "elb-target-group" ] ]
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 0
      UnhealthyThresholdCount: 3
      VpcId: !Select [0, !Ref vpcIdList]
  # Listener
  NLBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Condition: CreateNetworkLoadBalancer
    Properties:
      DefaultActions:
      - TargetGroupArn: !Ref NLBTargetGroup
        Type: forward
      LoadBalancerArn: !Ref NLB
      Port: 22
      Protocol: TCP
  ######################################################


  ######################################################
  # Classic ELB - for regions that do not yet support a Network LB.
  # ELB
  ELB:
    Type: "AWS::ElasticLoadBalancing::LoadBalancer"
    Condition: CreateClassicLoadBalancer
    Properties:
      Subnets: !Ref bastionSubnetList
      HealthCheck:
        HealthyThreshold: '3'
        Interval: '10'
        Target: TCP:22
        Timeout: '5'
        UnhealthyThreshold: '3'
      ConnectionSettings:
        IdleTimeout: '60'
      CrossZone: 'true'
      SecurityGroups:
      - Ref: BastionELBSG
      Listeners:
      - InstancePort: '22'
        LoadBalancerPort: '22'
        Protocol: TCP
        InstanceProtocol: TCP
      LoadBalancerName: !Join [ "-", [ !Ref awsTagsNamePrefix, "bastion-elb" ] ]
      Tags:
      - Key: Environment
        Value: !Ref awsTagsEnvironment
      - Key: Application
        Value: !Ref awsTagsApplication
      - Key: Name
        Value: !Join [ "-", [ !Ref awsTagsNamePrefix, "bastion-elb" ] ]

Via a Stack Update, we now want to add code to associate the existing Load Balancer (the one that was provisioned based on the region), to an AutoScaling group. To do this, AutoScaling has separate Properties for each (TargetGroupARNs for Network LoadBalancer Target Groups, and LoadBalancerNames for Classic ELBs). We're defining both in AWS::AutoScaling::AutoScalingGroup, and relying on the True/False value of the condition, an intrinsic function (Fn::If), and the AWS::NoValue pseudo parameter. In theory the below code should work.

TargetGroupARNs:
        !If
          - CreateNetworkLoadBalancer
          - !Ref NLBTargetGroup
          - !Ref "AWS::NoValue"
      LoadBalancerNames:
        !If
          - CreateClassicLoadBalancer
          - !Ref ELB
          - !Ref "AWS::NoValue"

However the syntax/format of the Yaml just isn't correct - We are getting the following error when running the stack (Value of property TargetGroupARNs must be of type List of String).

What is the proper way to tie the If, the conditions, and NoValue together so that a List of a single String (resulting in either a !Ref to NoValue or NLB) is assigned to the value of TargetGroupARNs?

BastionASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AvailabilityZones:
        Fn::GetAZs: !Ref "AWS::Region"
      Cooldown: '0'
      DesiredCapacity: '0'
      HealthCheckGracePeriod: '60'
      HealthCheckType: EC2
      MaxSize: '0'
      MinSize: '0'
      VPCZoneIdentifier: !Ref bastionSubnetList
      LaunchConfigurationName:
        Ref: BastionLC
      Tags:
      - Key: Environment
        Value: !Ref awsTagsEnvironment
        PropagateAtLaunch: true
      - Key: Application
        Value: !Ref awsTagsApplication
        PropagateAtLaunch: true
      - Key: Name
        Value: !Join [ "-", [ !Ref awsTagsNamePrefix, "asg-ec2" ] ]
        PropagateAtLaunch: true
      TargetGroupARNs:
        !If
          - CreateNetworkLoadBalancer
          - !Ref NLBTargetGroup
          - !Ref "AWS::NoValue"
      LoadBalancerNames:
        !If
          - CreateClassicLoadBalancer
          - !Ref ELB
          - !Ref "AWS::NoValue"
      TerminationPolicies:
      - OldestInstance
2

2 Answers

2
votes

I think you're better off creating two BastionASG resources using conditions on the resources

BastionASGElb:
  Type: AWS::AutoScaling::AutoScalingGroup
  Condition: CreateClassicLoadBalancer
  Properties:
    AvailabilityZones:
      Fn::GetAZs: !Ref "AWS::Region"
    Cooldown: '0'
    DesiredCapacity: '0'
    HealthCheckGracePeriod: '60'
    HealthCheckType: EC2
    MaxSize: '0'
    MinSize: '0'
    VPCZoneIdentifier: !Ref bastionSubnetList
    LaunchConfigurationName:
      Ref: BastionLC
    Tags:
    - Key: Environment
      Value: !Ref awsTagsEnvironment
      PropagateAtLaunch: true
    - Key: Application
      Value: !Ref awsTagsApplication
      PropagateAtLaunch: true
    - Key: Name
      Value: !Join [ "-", [ !Ref awsTagsNamePrefix, "asg-ec2" ] ]
      PropagateAtLaunch: true
    LoadBalancerNames:
      - !Ref ELB
    TerminationPolicies:
    - OldestInstance

BastionASGAlb:
  Type: AWS::AutoScaling::AutoScalingGroup
  Condition: CreateNetworkLoadBalancer
  Properties:
    AvailabilityZones:
      Fn::GetAZs: !Ref "AWS::Region"
    Cooldown: '0'
    DesiredCapacity: '0'
    HealthCheckGracePeriod: '60'
    HealthCheckType: EC2
    MaxSize: '0'
    MinSize: '0'
    VPCZoneIdentifier: !Ref bastionSubnetList
    LaunchConfigurationName:
      Ref: BastionLC
    Tags:
    - Key: Environment
      Value: !Ref awsTagsEnvironment
      PropagateAtLaunch: true
    - Key: Application
      Value: !Ref awsTagsApplication
      PropagateAtLaunch: true
    - Key: Name
      Value: !Join [ "-", [ !Ref awsTagsNamePrefix, "asg-ec2" ] ]
      PropagateAtLaunch: true
    TargetGroupARNs:  
      - !Ref NLBTargetGroup
    TerminationPolicies:
    - OldestInstance
1
votes

In YAML you can use two dashes - - to create a list of lists:

Prop:
  - - one
    - two
    - three
  - - red
    - blue

So for your !If statements bound to list properties you wrap the second value in a sublist:

TargetGroupARNs:
  !If
    - CreateNetworkLoadBalancer

    - - !Ref NLBTargetGroup

    - !Ref "AWS::NoValue"

Looking at my own scripts it appears the AWS::NoValue doesn't need to be wrapped, but if you had a different !Ref in the second block it would also need to be a sublist.