In an existing CloudFormation stack, if you have a load balancer and a target group attached via a listener, there is no way to be able to update both with changes that require replacements. Amazon says that their Name properties should be left blank, but then you'll get an error when you try to update the ALB. You can set the TG Name to incorporate that ALB's automatic Name, but then you'll get an error when you try to update the TG.
I'll give a lot more details in a wall of text below the template, but in short I've created a sample template which works around this issue. My question is, do people here think this is the best way to deal with the problem?
The updates in this template are done entirely through the parameters; changing the template is not necessary.
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
SetTargetGroupName:
Description: Configure the Target Group Name to be based on the Load Balancer Name. If false, then the Target Group Name is left unspecified.
Type: String
AllowedValues:
- false
- true
Default: false
LoadBalancerScheme:
Description: The Scheme of the Load Balancer.
Type: String
AllowedValues:
- internal
- internet-facing
Default: internal
TargetGroupPort:
Description: The Port of the Target Group.
Type: Number
Default: 80
MinValue: 1
MaxValue: 65535
Conditions:
DoSetTargetGroupName: !Equals
- !Ref SetTargetGroupName
- true
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/23
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
Subnet0:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.0.0/24
AvailabilityZone: !Select
- 0
- !GetAZs
Subnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select
- 1
- !GetAZs
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: !Ref LoadBalancerScheme
Subnets:
- !Ref Subnet0
- !Ref Subnet1
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !If
- DoSetTargetGroupName
- !Sub TG-${LoadBalancer.LoadBalancerName}
- !Ref AWS::NoValue
Port: !Ref TargetGroupPort
Protocol: HTTP
VpcId: !Ref VPC
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
Here is a wall of text with a bunch more details and reasoning. You can skip this if you want to:
= == = === = == = ==== = == = === = == =
Let's say you have an existing CloudFormation stack with an application load balancer and a target group, connected by a listener. And let's say you want to modify an property on that ALB or on that TG using a CloudFormation stack update, and let's say that modification requires replacing the ALB or the TG. Then you will have a problem. I've come up with a solution, and I'm wondering if it's the best way to do this.
In Amazon's documentation for AWS::ElasticLoadBalancingV2::LoadBalancer they say that the Name property needs to be left blank, or you need to modify your given name, if you want to perform an update that requires replacement of the resource. The documentation for AWS::ElasticLoadBalancingV2::TargetGroup doesn't say this, but in my testing I see that the same restriction applies.
When you've got a TG connected to an ALB and you leave both Names blank, you still can't replace the ALB. This is because Amazon will create a new ALB first, and then try to attach the existing TG to it (through the Listener) before it deletes anything. CloudFormation doesn't try to detach the TG first, and a TG can't be attached to more than one ALB at a time. The solution for this is to actually set the TG's Name property, but use the ALB's automatically-generated Name (or at least the random string part) as part of the name. This way, when CloudWatch creates the new ALB, this will result in the TG's Name changing, which causes a new TG to also be created. And that new TG can be attached to the new ALB.
But then, what if you want to modify a property of the TG which requires replacement? Now you've got a TG Name that you're setting (instead of leaving it blank so that CloudFormation generates a random Name). So when CloudFormation goes to create the replacement TG before deleting the old TG, it'll error out saying that you can't have two TG's with the same name.
So I've come up with an example template which allows for both kinds of updates, pasted below. It gives the user a parameter which sets the TG Name to be blank or to be based on the ALB name. And I think this example shows how this kind of parameter can be inserted into an existing stack if needed, so that someone can get out of a stuck situation without having to replace the whole stack now. The updates for this workaround are done entirely through the parameters; changing the template is not necessary.
If SetTargetGroupName is set to false, then the Load Balancer Scheme cannot be modified through a CloudFormation stack update. It will error out with the error below. But, if SetTargetGroupName gets set to true (it can be done at the same time as the Scheme modification), then the update will succeed.
AWS::ElasticLoadBalancingV2::Listener - Logical ID: Listener - Resource handler returned message: "The following target groups cannot be associated with more than one load balancer: arn:aws:elasticloadbalancing:us-east-2:692973256363:targetgroup/pgn-te-Targe-3I1DX4LDYYMZ/d6b64fadeabe2c6b (Service: ElasticLoadBalancingV2, Status Code: 400, Request ID: 759a63d7-a850-4066-8fb2-8758eb768a2e) (SDK Attempt Count: 1)" (RequestToken: 012f9faf-6156-ca7f-1ce0-f7a4925a30fd, HandlerErrorCode: ServiceLimitExceeded)
If SetTargetGroupName is set to true, then the Target Group Port cannot be modified through a CloudFormation stack update. It will error out with the error below. But, if SetTargetGroupName gets set to false (it can be done at the same time as the Port modification), then the update will succeed.
AWS::ElasticLoadBalancingV2::TargetGroup - Logical ID: TargetGroup - Resource handler returned message: "Resource of type 'AWS::ElasticLoadBalancingV2::TargetGroup' with identifier 'TG-pgn-te-LoadB-7uQlvsjubujb' already exists." (RequestToken: bca35472-3cbf-dbe9-7766-5ab1a8fa3d07, HandlerErrorCode: AlreadyExists)
The SetTargetGroupName parameter would probably need a pretty explanatory description, since this is an awkward workaround. Something like this:
Defaults to false. If an existing stack needs to be updated to modify a Target Group property, then this parameter should be set to false. If a update needs to modify a Load Balancer, then it should be set to true. Changing this parameter can be done in the same update as the TG or LB modification. If both a TG and an LB need to be modified, then you must first do one in one update, and then the other. If you get the error message "The following target groups cannot be associated with more than one load balancer" or "Resource of type 'AWS::ElasticLoadBalancingV2::TargetGroup' with identifier '***' already exists", then try flipping this parameter and see if it helps.
In my testing, I don't think there's any real downside to this workaround in the way that changes are rolled out or the speed of them or anything. CloudFormation takes care of retaining as much uptime as is possible while replacing the load balancer or the target group. And the ALB and TG should always be given a Name tag, so that the resource Name never needs to be anything that's readable by any human.