Or you need to allow UDP access which the Elastic Load Balancers (ELB) and Network Load Balancers (NLB) don't allow. This is for you.
What this does is listen to a Simple Notification Service (SNS) to any published events which the ASG would send for adding an instance to the pool or terminating an instance to the pool. It then queries the asg looking for the tag DomainMeta and then with the list of ec2 instances it goes and collects the public ip address and goes to the route53 zone that is recorded and updates the domain attached.
The tag should be in the format DomainMeta:
Where the first part is the hosted zone it needs to end the command to and the second part is the a record it is going to change.
This is based on the work that objectpartners.com did back in 2005. I've improved it to include security so that only one hosted zone is looked after or allows full account control if you are 100% in control of the tags on the ASG's.
This could easily be updated to include a coma delimited list on the tag to update multiple a records if required.
Please note: If the last instance is taken out of the pool the old ip address will be left since it route53 records can't be null/empty.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
AWSTemplateFormatVersion: '2010-09-09' | |
Transform: 'AWS::Serverless-2016-10-31' | |
Description: AutoScaling Group to Route 53 record update | |
#ensure you have the Tag DomainMeta set which a value of <HostedZoneId>:<Domain> on the ASG i.e. DomainMeta: Z10MWC8V7JDDU1:www.mydomain.com | |
Parameters: | |
Service: | |
Type: String | |
Default: 'asgToRoute53' | |
Description: Service name for this product | |
HostedZone: | |
Type: AWS::Route53::HostedZone::Id | |
Description: This is the hosted zone which this lambda function can edit, unless allHostedZone is true i.e. arn:aws:route53:::hostedzone/${HostedZone} | |
AllowAllHostedZoneControl: | |
Type: String | |
Default: "false" | |
AllowedValues : | |
- "true" | |
- "false" | |
Description: If true, then HostedZone is not used and wild card will be set for iam policy | |
Conditions: | |
hasRestrictedRoute53Control: | |
!Equals [ !Ref AllowAllHostedZoneControl, 'false' ] | |
hasWildCardRoute53Control: | |
!Equals [ !Ref AllowAllHostedZoneControl, 'true' ] | |
Resources: | |
ASGtoRoute53UpdateSNSTopic: | |
Type: AWS::SNS::Topic | |
Properties: | |
DisplayName: ASGtoRoute53Update | |
LambdaASGtoRoute53Update: | |
Type: 'AWS::Serverless::Function' | |
DependsOn: | |
- LambdaASGtoRoute53UpdateRole | |
Properties: | |
#FunctionName: LambdaASGtoRoute53Update | |
InlineCode: | | |
var AWS=require("aws-sdk"),nextTick=function(e){"function"==typeof setImmediate?setImmediate(e):"undefined"!=typeof process&&process.nextTick?process.nextTick(e):setTimeout(e,0)},makeIterator=function(e){var n=function(o){var t=function(){return e.length&&e[o].apply(null,arguments),t.next()};return t.next=function(){return o<e.length-1?n(o+1):null},t};return n(0)},_isArray=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)},waterfall=function(e,n){if(n=n||function(){},!_isArray(e)){var o=new Error("First argument to waterfall must be an array of functions");return n(o)}if(!e.length)return n();var t=function(e){return function(o){if(o)n.apply(null,arguments),n=function(){};else{var r=Array.prototype.slice.call(arguments,1),s=e.next();s?r.push(t(s)):r.push(n),nextTick(function(){e.apply(null,r)})}}};t(makeIterator(e))()}; | |
exports.handler=function(e,n){console.log(e);var o=JSON.parse(e.Records[0].Sns.Message),t=o.AutoScalingGroupName,r=(o.EC2InstanceId,o.Event);if(console.log(r),"autoscaling:EC2_INSTANCE_LAUNCH"===r||"autoscaling:EC2_INSTANCE_TERMINATE"===r){console.log("Handling Launch/Terminate Event for "+t);var s=process.env.AWS_DEFAULT_REGION;console.log(s);var a=new AWS.AutoScaling({region:s}),c=new AWS.EC2({region:s}),l=new AWS.Route53; | |
waterfall([function(e){console.log("Describing ASG Tags"),a.describeTags({Filters:[{Name:"auto-scaling-group",Values:[t]},{Name:"key",Values:["DomainMeta"]}],MaxRecords:1},e)}, | |
function(e,n){console.log("Processing ASG Tags"),console.log(e.Tags),0==e.Tags.length&&n("ASG: "+t+" does not define Route53 DomainMeta tag.");var o=e.Tags[0].Value.split(":"),r={HostedZoneId:o[0],RecordName:o[1]};console.log(r),n(null,r)}, | |
function(e,n){console.log("Retrieving Instances in ASG"),a.describeAutoScalingGroups({AutoScalingGroupNames:[t],MaxRecords:1},function(o,t){n(o,e,t)})}, | |
function(e,n,o){console.log(n.AutoScalingGroups[0]);var t=n.AutoScalingGroups[0].Instances.map(function(e){return e.InstanceId});c.describeInstances({DryRun:!1,InstanceIds:t}, | |
function(n,t){o(n,e,t)})},function(e,n,o){console.log(n.Reservations);var t=n.Reservations.map(function(e){return{Value:e.Instances[0].NetworkInterfaces[0].Association.PublicIp}});console.log(t),l.changeResourceRecordSets({ChangeBatch:{Changes:[{Action:"UPSERT",ResourceRecordSet:{Name:e.RecordName,Type:"A",TTL:10,ResourceRecords:t}}]},HostedZoneId:e.HostedZoneId},o)}], | |
function(e){e?console.error("Failed to process DNS updates for ASG event: ",e):console.log("Successfully processed DNS updates for ASG event."),n.done(e)})}else console.log("Unsupported ASG event: "+t,r),n.done("Unsupported ASG event: "+t,r)}; | |
Description: !Sub 'Asg to Route53 record domain update' | |
Handler: index.handler | |
MemorySize: 128 | |
Role: !GetAtt LambdaASGtoRoute53UpdateRole.Arn | |
Runtime: nodejs8.10 | |
Timeout: 20 | |
Events: | |
SNS1: | |
Type: SNS | |
Properties: | |
Topic: | |
Ref: ASGtoRoute53UpdateSNSTopic | |
# Has either AsgToRoute53EditPolicy attached for route53 edit rights | |
LambdaASGtoRoute53UpdateRole: | |
Type: AWS::IAM::Role | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: 2012-10-17 | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: [lambda.amazonaws.com] | |
Action: | |
- sts:AssumeRole | |
ManagedPolicyArns: | |
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole | |
- arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess | |
#is run when restricted access is set | |
AsgToRoute53EditPolicy: | |
Condition: hasRestrictedRoute53Control | |
DependsOn: LambdaASGtoRoute53UpdateRole | |
Type: AWS::IAM::Policy | |
Properties: | |
PolicyName: "AsgToRoute53EditPolicy" | |
Roles: | |
- | |
!Ref LambdaASGtoRoute53UpdateRole | |
PolicyDocument: | |
Version: "2012-10-17" | |
Statement: | |
- Effect: Allow | |
Action: | |
- route53:ListHostedZones | |
- route53:GetChange | |
Resource: | |
- "*" | |
- Effect: Allow | |
Action: | |
- route53:ChangeResourceRecordSets | |
Resource: | |
- !Sub "arn:aws:route53:::hostedzone/${HostedZone}" | |
#is run when allowed to change all route53 in this account | |
AsgToRoute53EditPolicy: | |
Condition: hasWildCardRoute53Control | |
DependsOn: LambdaASGtoRoute53UpdateRole | |
Type: AWS::IAM::Policy | |
Properties: | |
PolicyName: "AsgToRoute53EditPolicy" | |
Roles: | |
- | |
!Ref LambdaASGtoRoute53UpdateRole | |
PolicyDocument: | |
Version: "2012-10-17" | |
Statement: | |
- Effect: Allow | |
Action: | |
- route53:ListHostedZones | |
- route53:GetChange | |
Resource: | |
- "*" | |
- Effect: Allow | |
Action: | |
- route53:ChangeResourceRecordSets | |
Resource: | |
- !Sub "arn:aws:route53:::hostedzone/*" | |
Outputs: | |
ASGtoRoute53UpdateSNSTopic: | |
Description: ARN of newly created SNS Topic | |
Value: | |
Ref: ASGtoRoute53UpdateSNSTopic | |
Export: | |
Name: !Sub "${Service}-sns" | |
QueueName: | |
Description: Name of newly created SNS Topic | |
Value: | |
Fn::GetAtt: | |
- ASGtoRoute53UpdateSNSTopic | |
- TopicName |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//# ================================================================================================== | |
//# Function: Asg to Route53 record domain update | |
//# Purpose: Update domain with new list of public ip addresses of the asg | |
//# ================================================================================================== | |
var AWS=require("aws-sdk"); | |
// MIT license (by Elan Shanker). | |
//https://objectpartners.com/2015/07/07/aws-tricks-updating-route53-dns-for-autoscalinggroup-using-lambda/ | |
// with https://github.com/es128/async-waterfall/blob/master/index.js | |
'use strict'; | |
var nextTick = function (fn) { | |
if (typeof setImmediate === 'function') { | |
setImmediate(fn); | |
} else if (typeof process !== 'undefined' && process.nextTick) { | |
process.nextTick(fn); | |
} else { | |
setTimeout(fn, 0); | |
} | |
}; | |
var makeIterator = function (tasks) { | |
var makeCallback = function (index) { | |
var fn = function () { | |
if (tasks.length) { | |
tasks[index].apply(null, arguments); | |
} | |
return fn.next(); | |
}; | |
fn.next = function () { | |
return (index < tasks.length - 1) ? makeCallback(index + 1): null; | |
}; | |
return fn; | |
}; | |
return makeCallback(0); | |
}; | |
var _isArray = Array.isArray || function(maybeArray){ | |
return Object.prototype.toString.call(maybeArray) === '[object Array]'; | |
}; | |
var waterfall = function (tasks, callback) { | |
callback = callback || function () {}; | |
if (!_isArray(tasks)) { | |
var err = new Error('First argument to waterfall must be an array of functions'); | |
return callback(err); | |
} | |
if (!tasks.length) { | |
return callback(); | |
} | |
var wrapIterator = function (iterator) { | |
return function (err) { | |
if (err) { | |
callback.apply(null, arguments); | |
callback = function () {}; | |
} else { | |
var args = Array.prototype.slice.call(arguments, 1); | |
var next = iterator.next(); | |
if (next) { | |
args.push(wrapIterator(next)); | |
} else { | |
args.push(callback); | |
} | |
nextTick(function () { | |
iterator.apply(null, args); | |
}); | |
} | |
}; | |
}; | |
wrapIterator(makeIterator(tasks))(); | |
}; | |
exports.handler = function (event, context) { | |
console.log(event); | |
var asg_msg = JSON.parse(event.Records[0].Sns.Message); | |
var asg_name = asg_msg.AutoScalingGroupName; | |
var instance_id = asg_msg.EC2InstanceId; | |
var asg_event = asg_msg.Event; | |
console.log(asg_event); | |
if (asg_event === "autoscaling:EC2_INSTANCE_LAUNCH" || asg_event === "autoscaling:EC2_INSTANCE_TERMINATE") { | |
console.log("Handling Launch/Terminate Event for " + asg_name); | |
var region = process.env.AWS_DEFAULT_REGION | |
console.log(region) | |
var autoscaling = new AWS.AutoScaling({region: region}); // ${AWS::Region} | |
var ec2 = new AWS.EC2({region: region}); // ${AWS::Region} | |
var route53 = new AWS.Route53(); | |
waterfall([ | |
function describeTags(next) { | |
console.log("Describing ASG Tags"); | |
autoscaling.describeTags({ | |
Filters: [ | |
{ | |
Name: "auto-scaling-group", | |
Values: [ | |
asg_name | |
] | |
}, | |
{ | |
Name: "key", | |
Values: ['DomainMeta'] | |
} | |
], | |
MaxRecords: 1 | |
}, next); | |
}, | |
function processTags(response, next) { | |
console.log("Processing ASG Tags"); | |
console.log(response.Tags); | |
if (response.Tags.length == 0) { | |
next("ASG: " + asg_name + " does not define Route53 DomainMeta tag."); | |
} | |
var tokens = response.Tags[0].Value.split(':'); | |
var route53Tags = { | |
HostedZoneId: tokens[0], | |
RecordName: tokens[1] | |
}; | |
console.log(route53Tags); | |
next(null, route53Tags); | |
}, | |
function retrieveASGInstances(route53Tags, next) { | |
console.log("Retrieving Instances in ASG"); | |
autoscaling.describeAutoScalingGroups({ | |
AutoScalingGroupNames: [asg_name], | |
MaxRecords: 1 | |
}, function(err, data) { | |
next(err, route53Tags, data); | |
}); | |
}, | |
function retrieveInstanceIds(route53Tags, asgResponse, next) { | |
console.log(asgResponse.AutoScalingGroups[0]); | |
var instance_ids = asgResponse.AutoScalingGroups[0].Instances.map(function(instance) { | |
return instance.InstanceId | |
}); | |
ec2.describeInstances({ | |
DryRun: false, | |
InstanceIds: instance_ids | |
}, function(err, data) { | |
next(err, route53Tags, data); | |
}); | |
}, | |
function updateDNS(route53Tags, ec2Response, next) { | |
console.log(ec2Response.Reservations); | |
var resource_records = ec2Response.Reservations.map(function(reservation) { | |
return { | |
Value: reservation.Instances[0].NetworkInterfaces[0].Association.PublicIp | |
}; | |
}); | |
console.log(resource_records); | |
route53.changeResourceRecordSets({ | |
ChangeBatch: { | |
Changes: [ | |
{ | |
Action: 'UPSERT', | |
ResourceRecordSet: { | |
Name: route53Tags.RecordName, | |
Type: 'A', | |
TTL: 10, | |
ResourceRecords: resource_records | |
} | |
} | |
] | |
}, | |
HostedZoneId: route53Tags.HostedZoneId | |
}, next); | |
} | |
], function (err) { | |
if (err) { | |
console.error('Failed to process DNS updates for ASG event: ', err); | |
} else { | |
console.log("Successfully processed DNS updates for ASG event."); | |
} | |
context.done(err); | |
}) | |
} else { | |
console.log("Unsupported ASG event: " + asg_name, asg_event); | |
context.done("Unsupported ASG event: " + asg_name, asg_event); | |
} | |
}; |