Skip to content

Instantly share code, notes, and snippets.

@duttonw
Last active March 31, 2019 00:25
Show Gist options
  • Save duttonw/798df82271907b45b593a36bb1b2044a to your computer and use it in GitHub Desktop.
Save duttonw/798df82271907b45b593a36bb1b2044a to your computer and use it in GitHub Desktop.
AutoScaling Group to update Route53 hostedZoneID by tag, asgUpdateRoute53.js is the un-minified code in the tamplte
---
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
//# ==================================================================================================
//# 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);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment