Create an elastic load balancer and update Route 53 DNS in PowerShell

R53-elb

The number one rule to control AWS costs is: If you aren’t using it, shut it down.

For some things, that’s easy. EC2 instances themselves can be stopped and started at will, by script or automation. But Elastic Load Balancers (ELB) cannot be stopped or started. They exist or they don’t. And when they do exist, they incur two types of charges: bandwidth and an hourly charge. It’s not expensive. But if you have lots of development and/or test environments using load balancing that you are stopping when not in use but the ELBs remain, costs can add up.

If this is your situation, the PowerShell script below might be of interest to you. My client currently has multiple environments that are spun up and shut down using PowerShell and they also use AWS Route 53 for DNS management. I decided to develop a PowerShell script that would:

  • Create an EC2 ELB with a health check and tags for billing purposes and
  • Update the Route 53 resource record set as a Route 53 “alias” for the ELB name.

The PowerShell script I came up with is below. I hope you find it useful.

Here are some usage notes.

Apparently, it’s perfectly OK to issue New-ELBLoadbalancer for an existing ELB name. Your changes, if any, are applied to the existing load balancer. That’s why I didn’t bother to issue Remove-ELBLoadBalancer first.

The ELB in this example did not require a cert, so [Amazon.ElasticLoadBalancing.Model.Listener]::SSLCertificateID wasn’t required in the call to New-ELBLoadBalancer. If you do need to provide a cert, you will need its ARN. If you are using a DigiCert wildcard cert (and if you aren’t, you should be), you can cheat and get its ARN by issuing

(Get-ELBLoadBalancer -LoadBalancerName "CaseSensitiveELBName").ListenerDescriptions.Listener.SSLCertificateID

against a different ELB that’s using the same certificate. Oddly, unlike most EC2 objects which use identifiers, with the ELB cmdlets you must specify a case-sensitive name. I’ve complained before about having to retrieve an identifier for an EC2 object instead of using its name so I guess this is the exception to the rule I was looking for. (Even stranger, nothing prevents you from adding a tag key called “Name” to your ELB using Amazon.ElasticLoadBalancing.Model.Tag. This tag is not, of course, the same thing as the ELB’s name, so if you do add a tag key called “Name” to an ELB — and I do — just make sure that the tag value and the ELB name are identical.)

You will need the canonical name of the new ELB when you update the Route 53 resource record set.  New-ELBLoadBalancer returns a string with the DNS name of the new ELB, not an object of type Amazon.ElasticLoadBalancing.Model.LoadBalancerDescription. So, that’s why you see a Get-ELBLoadBalancer cmdlet to retrieve this value.

Route 53 cmdlets feel very different from the familiar EC2 cmdlets, at least to me. Maybe it’s because Route 53 is “early” (the API doc is dated 2013) or maybe it’s because of the nature of managing zone files. But I’d be less than honest if I didn’t say that accomplishing even the simple task of updating an existing resource record set with a Route 53 ELB alias was a little challenging.

First, there is the triple nested object requirement for
Edit-R53ResourceRecordSet. I also found it a bit quaint that Route 53 requires “batches” of changes. It’s all very lovely once you figure it out. But that took me a little doing. 🙂

The other big obstacle, at least for me at first, was the fact that unlike the ELB cmdlets, Edit-R53ResourceRecordSet must know whether the resource record set is new or an update. Even though ‘UPSERT’ first deletes the record set and then adds it, you must know if the record exists before issuing the cmdlet. I used a rather complex call to Get-R53ResourceRecordSet to figure this out.

One issue with this script is the fact that public DNS entries take time to propagate. I didn’t address this in this script. But I did write a small loop to make sure that at least Route 53 says its DNS servers are all consistent.

Once again, I hope this helps you and I look forward to your feedback.

<#Copyright 2016 Air11 Technology LLC
 
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
 
    http://www.apache.org/licenses/LICENSE-2.0
 
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. #>
 

Import-Module AWSPowerShell
$ELBName = 'YourDesiredELBName'
# Create listener object for New-ELBLoadBalancer
$ELBListener = New-Object -TypeName Amazon.ElasticLoadBalancing.Model.Listener
$ELBListener.InstancePort = 443
# This is a TCP TLS passthrough ELB, so no cert is needed on the ELB; uncomment and supply
#$ELBListener.SSLCertificateId = "arn:aws:iam::111111111111:server-certificate/wildcard.your.domain.cert.com"
$ELBListener.LoadBalancerPort = 443
$ELBListener.Protocol = "TCP"
$ELBListener.InstanceProtocol = "TCP"

# Create tag objects for New-ELBLoadBalancer
$ELBTag1 = New-Object -TypeName Amazon.ElasticLoadBalancing.Model.Tag
$ELBTag1.Key = 'YourTagKey1'
$ELBTag1.Value = 'YourTagValue1'
$ELBTag2 = New-Object -TypeName Amazon.ElasticLoadBalancing.Model.Tag
$ELBTag2.Key = 'YourTagKey2'
$ELBTag2.Value = 'YourTagValue2'

#Create a new ELB and save the DNS name for updating in R53; if it exists, we just continue along as AWS doesn't duplicate the name or issue an error
#  You can issue Add-ELB tags after issuing New-ELBLoadBalancer, but I like getting everything done in one API call
$ELBDNS = New-ELBLoadBalancer -LoadBalancerName $ELBName -Scheme 'internet-facing' -SecurityGroup 'sg-12345678', 'sg-87654321' -Subnet 'subnet-12345678', 'subnet-87654321' -Tag $ELBTag1, $ELBTag2  -listener $ELBListener
# Setup the healthcheck
Set-ELBHealthCheck -LoadBalancerName $ELBName -HealthCheck_Interval 30 -HealthCheck_Timeout 5 -HealthCheck_UnhealthyThreshold 2 -HealthCheck_HealthyThreshold 3 -HealthCheck_Target 'TCP:443'
$CannonicalID = (Get-ELBLoadBalancer $ELBName).CanonicalHostedZoneNameID #Get Cannonical ID of newly created ELB which is required to create resource record set

$zoneName = 'aws.example.com'
$subDomainName = 'sub'
$resourceName = $subDomainName + "." + $zoneName

$R53change = New-Object -TypeName Amazon.Route53.Model.Change # Create object used with Edit-R53ResourceRecordSet
$R53change.ResourceRecordSet = New-Object Amazon.Route53.Model.ResourceRecordSet # Create embedded object in request
$R53change.ResourceRecordSet.Name = $resourceName
$R53change.ResourceRecordSet.Type = "A" # Address record
$R53change.ResourceRecordSet.AliasTarget = New-Object Amazon.Route53.Model.AliasTarget # This entry is an alias entry to an ELB
$R53change.ResourceRecordSet.AliasTarget.HostedZoneId = $CannonicalID # ELB canonical zoneid
$R53change.ResourceRecordSet.AliasTarget.DNSName = $ELBDNS # DNS of ELB
$R53change.ResourceRecordSet.AliasTarget.EvaluateTargetHealth = $false

# This complicated statement's purpose is to get a trimmed ZoneID for us in the R53 change batch. It gets the zones, finds our specific hosted zone, then trims out the just the zone ID into a string variable
$R53ZoneID = (Get-R53HostedZones | Where-Object Name -eq ($zoneName + '.') | Select-Object -ExpandProperty ID).TrimStart("/hostedzone/")


# This rather nasty if statement is to determine if the resource record exists. It does that by getting all resource record sets, looking for the
#   resource name WHICH MUST END IN A PERIOD then piping the result to Measure-Object to count it. Zero means it doesn't exist.
#   PS syntax specifies that only the FIRST element of the pipline can be an expression, so instead of (Measure-Object).Count we must call the cmdlet as part of a single expression in the if {} statement
if ((((Get-R53ResourceRecordSet -HostedZoneId $R53ZoneID).ResourceRecordSets | Where-Object Name -eq ($resourceName + '.')) | Measure-Object).Count -eq 0)
{
    # It doesn't exist, so create it.
    $R53change.Action = 'CREATE'
}
else
{
    #It's there, so update it
    $R53change.Action = 'UPSERT'
}


# Update Route53
$now = Get-Date -f -- FileDateTimeUniversal
$changeInfo = Edit-R53ResourceRecordSet -HostedZoneId $R53ZoneID -ChangeBatch_Change $R53change -ChangeBatch_Comment "Added/updated $now"

# While the full Internet might not get the DNS updates for some time, we shouldn't continue until at least AWS's DNS servers say they are up-to-date
While ((Get-R53Change -Id $changeInfo.id).Status -eq 'PENDING')
{
    Start-Sleep 5
}

 


Posted

in

, , ,

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *