├── .gitignore ├── .jshintignore ├── .jshintrc ├── LICENSE ├── Makefile ├── README.md ├── cli.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | test/** 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 500, 3 | "bitwise" : true, 4 | "camelcase" : false, 5 | "curly" : true, 6 | "eqeqeq" : true, 7 | "forin" : true, 8 | "immed" : false, 9 | "indent" : 4, 10 | "latedef" : true, 11 | "newcap" : false, 12 | "noarg" : true, 13 | "noempty" : true, 14 | "nonew" : false, 15 | "plusplus" : false, 16 | "quotmark" : false, 17 | "undef" : true, 18 | "unused" : false, 19 | "strict" : true, 20 | "trailing" : true, 21 | "maxparams" : false, 22 | "maxdepth" : false, 23 | "maxstatements" : false, 24 | "maxcomplexity" : false, 25 | "maxlen" : false, 26 | "asi" : false, 27 | "boss" : false, 28 | "debug" : false, 29 | "eqnull" : false, 30 | "es5" : false, 31 | "esnext" : false, 32 | "moz" : false, 33 | "evil" : false, 34 | "expr" : false, 35 | "funcscope" : false, 36 | "globalstrict" : false, 37 | "iterator" : false, 38 | "lastsemic" : false, 39 | "laxbreak" : false, 40 | "laxcomma" : false, 41 | "loopfunc" : false, 42 | "multistr" : false, 43 | "proto" : false, 44 | "scripturl" : false, 45 | "shadow" : false, 46 | "sub" : false, 47 | "supernew" : false, 48 | "validthis" : false, 49 | "browser" : false, 50 | "couch" : false, 51 | "devel" : false, 52 | "dojo" : false, 53 | "jquery" : false, 54 | "mootools" : false, 55 | "node" : true, 56 | "nonstandard" : false, 57 | "prototypejs" : false, 58 | "rhino" : false, 59 | "worker" : false, 60 | "wsh" : false, 61 | "yui" : false, 62 | "globals" : {}, 63 | "predef" : ["describe", "it"] 64 | } 65 | 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 widdix GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | jshint: 4 | @echo "jshint" 5 | @find . -name "*.js" -print0 | xargs -0 ./node_modules/.bin/jshint 6 | 7 | circular: 8 | @echo "circular" 9 | @./node_modules/.bin/madge --circular --format amd --exclude "madge|source-map" . 10 | 11 | mocha: 12 | @echo "mocha (unit test)" 13 | #@TZ=UTC ./node_modules/.bin/mocha test/*.js 14 | @echo 15 | 16 | coverage: 17 | @echo "cover" 18 | @./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha test/* 19 | @echo 20 | 21 | test: jshint mocha circular 22 | @echo "test" 23 | @echo 24 | 25 | outdated: 26 | @echo "outdated modules?" 27 | @./node_modules/.bin/npmedge 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/widdix/node-route53-updater.png)](http://travis-ci.org/widdix/node-route53-updater) 2 | [![NPM version](https://badge.fury.io/js/route53-updater.png)](http://badge.fury.io/js/route53-updater) 3 | [![NPM dependencies](https://david-dm.org/widdix/node-route53-updater.png)](https://david-dm.org/widdix/node-route53-updater) 4 | 5 | # route53-updater 6 | 7 | The `route53-updater` module can update a Route 53 Record Set with the current IP or hostname of an machine. This can be useful if you have a single instance running in an auto scaling group. During startup of the EC2 instance you call the `route53-updater` to update the DNS entry to the new IP. 8 | 9 | Port of https://github.com/taimos/route53-updater/ 10 | 11 | ## Usage 12 | 13 | Install route53-updater globally 14 | 15 | npm install route53-updater -g 16 | 17 | Create or update the DNS CNAME entry for test.yourdomain.com to point to the public hostname of the EC2 instance 18 | 19 | route53-updater --action UPDATE --hostedZoneName yourdomain.com. --recordSetName test.yourdomain.com. 20 | 21 | or 22 | 23 | route53-updater --action UPDATE --hostedZoneId XXXXXXXXXXXXX --recordSetName test.yourdomain.com. 24 | 25 | The assumed defaults are 26 | 27 | route53-updater --action UPDATE --hostedZoneName yourdomain.com. --recordSetName test.yourdomain.com. --ttl 60 --metadata public-hostname --type CNAME 28 | 29 | By default route53-updater will lookup the IP address against the Amazon Metadata Service. If running outside Amazon, you can use the first IPv4 address on an interface by specifying an --iface option 30 | 31 | route53-updater --action UPDATE --hostedZoneName yourdomain.com. --recordSetName test.yourdomain.com. --iface eth0 32 | 33 | The instance running the script needs the following IAM access rights: 34 | 35 | { 36 | "Version": "2012-10-17", 37 | "Statement": [ 38 | { 39 | "Sid": "Stmt1424083772000", 40 | "Effect": "Allow", 41 | "Action": [ 42 | "route53:ChangeResourceRecordSets", 43 | "route53:ListHostedZones", 44 | "route53:ListResourceRecordSets", 45 | "route53:GetChange" 46 | ], 47 | "Resource": [ 48 | "*" 49 | ] 50 | } 51 | ] 52 | } 53 | 54 | Supported parameters: 55 | 56 | * `action`: String (required) 57 | * `UPDATE`: Update the DNS entry (delete if exists, and create) 58 | * `DELETE`: Create the DNS entry 59 | * `CREATE`: Create the DNS entry or fail if existing 60 | * `hostedZoneName`: String (either `hostedZoneName` or `hostedZoneId` is required) - Name of your hosted zone (Must end with an dot!) 61 | * `hostedZoneId`: String (either `hostedZoneName` or `hostedZoneId` is required) - Id of your hosted zone 62 | * `recordSetName`: String (required) - Name of your record set (XYZ.hostedZoneName) 63 | * `ttl`: Number (optional, default 60) - TTL in seconds 64 | * `metadata`: String (optional, default public-hostname) - Metadata field to use as the value (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) 65 | * `type`: String (optional, default CNAME) - Type of record set (http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html) 66 | 67 | 68 | ## Breaking changes 69 | 70 | ### Update from 0.2.X to 1.0.X 71 | 72 | No breaking changes! 73 | 74 | ### Update from 0.1.X to 0.2.X 75 | 76 | Added `"route53:GetChange"` to the IAM access rights. 77 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require("minimist")(process.argv.slice(2)); 4 | var route53updater = require("./index.js"); 5 | 6 | route53updater(argv.action, argv, function(err) { 7 | "use strict"; 8 | if (err) { 9 | console.error(err.message, err.stack); 10 | process.exit(1); 11 | } else { 12 | process.exit(0); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert-plus"); 2 | var AWS = require("aws-sdk"); 3 | var async = require("async"); 4 | var underscore = require("underscore"); 5 | var os = require("os"); 6 | 7 | function retrieveHostedZoneId(name, cb) { 8 | "use strict"; 9 | assert.string(name, "name"); 10 | assert.func(cb, "cb"); 11 | var route53 = new AWS.Route53(), 12 | hostedZones = [], 13 | nextMarker; 14 | async.whilst( 15 | function() { 16 | if (nextMarker === null) { 17 | return false; 18 | } 19 | return true; 20 | }, 21 | function(cb) { 22 | route53.listHostedZones({ 23 | "Marker": nextMarker, 24 | "MaxItems": "50" 25 | }, function(err, res) { 26 | if (err) { 27 | cb(err); 28 | } else { 29 | hostedZones = hostedZones.concat(underscore.filter(res.HostedZones, function(hostedZone) { 30 | return hostedZone.Name === name; 31 | })); 32 | if (res.IsTruncated === true) { 33 | nextMarker = res.NextMarker; 34 | } else { 35 | nextMarker = null; 36 | } 37 | cb(); 38 | } 39 | }); 40 | }, 41 | function(err) { 42 | if (err) { 43 | cb(err); 44 | } else { 45 | if (hostedZones.length > 1) { 46 | cb(new Error("hostedZoneName not unique. Use hostedZoneId parameter.")); 47 | } else if (hostedZones.length === 1) { 48 | cb(undefined, hostedZones[0].Id); 49 | } else { 50 | cb(new Error("hostedZoneName not found")); 51 | } 52 | 53 | } 54 | } 55 | ); 56 | } 57 | 58 | function retrieveRecordSet(hostedZoneId, name, cb) { 59 | "use strict"; 60 | assert.string(hostedZoneId, "hostedZoneId"); 61 | assert.string(name, "name"); 62 | assert.func(cb, "cb"); 63 | var route53 = new AWS.Route53(); 64 | route53.listResourceRecordSets({ 65 | "HostedZoneId": hostedZoneId, 66 | "StartRecordName": name, 67 | "MaxItems": "1" 68 | }, function(err, res) { 69 | if (err) { 70 | cb(err); 71 | } else { 72 | cb(undefined, underscore.find(res.ResourceRecordSets, function(recordSet) { 73 | return recordSet.Name === name; 74 | })); 75 | } 76 | }); 77 | } 78 | 79 | function checkINSYNC(changeInfo, cb) { 80 | "use strict"; 81 | assert.object(changeInfo, "changeInfo"); 82 | assert.string(changeInfo.Id, "changeInfo.Id"); 83 | assert.string(changeInfo.Status, "changeInfo.Status"); 84 | assert.func(cb, "cb"); 85 | if (changeInfo.Status === "PENDING") { 86 | setTimeout(function() { 87 | var route53 = new AWS.Route53(); 88 | route53.getChange({ 89 | "Id": changeInfo.Id 90 | }, function(err ,res) { 91 | if (err) { 92 | cb(err); 93 | } else { 94 | checkINSYNC(res.ChangeInfo, cb); 95 | } 96 | }); 97 | }, 5000); 98 | } else if (changeInfo.Status === "INSYNC") { 99 | cb(); 100 | } else { 101 | cb(new Error("unsupported status " + changeInfo.Status)); 102 | } 103 | } 104 | 105 | function deleteRecordSet(hostedZoneId, name, cb) { 106 | "use strict"; 107 | assert.string(hostedZoneId, "hostedZoneId"); 108 | assert.string(name, "name"); 109 | assert.func(cb, "cb"); 110 | var route53 = new AWS.Route53(); 111 | retrieveRecordSet(hostedZoneId, name, function(err, recordSet) { 112 | if (err) { 113 | cb(err); 114 | } else { 115 | route53.changeResourceRecordSets({ 116 | "ChangeBatch": { 117 | "Changes": [{ 118 | "Action": "DELETE", 119 | "ResourceRecordSet": recordSet 120 | }], 121 | "Comment": "reoute53-updater deleteRecordSet()" 122 | }, 123 | "HostedZoneId": hostedZoneId 124 | }, function(err, res) { 125 | if (err) { 126 | cb(err); 127 | } else { 128 | checkINSYNC(res.ChangeInfo, cb); 129 | } 130 | }); 131 | } 132 | }); 133 | } 134 | 135 | function createRecordSet(hostedZoneId, name, type, value, ttl, cb) { 136 | "use strict"; 137 | assert.string(hostedZoneId, "hostedZoneId"); 138 | assert.string(name, "name"); 139 | assert.string(type, "type"); 140 | assert.string(value, "value"); 141 | assert.number(ttl, "ttl"); 142 | assert.func(cb, "cb"); 143 | var route53 = new AWS.Route53(); 144 | route53.changeResourceRecordSets({ 145 | "ChangeBatch": { 146 | "Changes": [{ 147 | "Action": "CREATE", 148 | "ResourceRecordSet": { 149 | "Name": name, 150 | "Type": type, 151 | "TTL": ttl, 152 | "ResourceRecords": [{ 153 | "Value": value 154 | }] 155 | } 156 | }], 157 | "Comment": "reoute53-updater createRecordSet()" 158 | }, 159 | "HostedZoneId": hostedZoneId 160 | }, function(err, res) { 161 | if (err) { 162 | cb(err); 163 | } else { 164 | checkINSYNC(res.ChangeInfo, cb); 165 | } 166 | }); 167 | } 168 | 169 | function updateRecordSet(hostedZoneId, name, type, value, ttl, cb) { 170 | "use strict"; 171 | assert.string(hostedZoneId, "hostedZoneId"); 172 | assert.string(name, "name"); 173 | assert.string(type, "type"); 174 | assert.string(value, "value"); 175 | assert.number(ttl, "ttl"); 176 | assert.func(cb, "cb"); 177 | var route53 = new AWS.Route53(); 178 | var params = { 179 | "ChangeBatch": { 180 | "Changes": [{ 181 | "Action": "UPSERT", 182 | "ResourceRecordSet": { 183 | "Name": name, 184 | "Type": type, 185 | "TTL": ttl, 186 | "ResourceRecords": [{ 187 | "Value": value 188 | }] 189 | } 190 | }], 191 | "Comment": "reoute53-updater updateRecordSet()" 192 | }, 193 | "HostedZoneId": hostedZoneId 194 | }; 195 | route53.changeResourceRecordSets(params, function(err, res) { 196 | if (err) { 197 | cb(err); 198 | } else { 199 | checkINSYNC(res.ChangeInfo, cb); 200 | } 201 | }); 202 | } 203 | 204 | function run(action, hostedZoneId, params, cb) { 205 | "use strict"; 206 | if (action === "CREATE" || action === "UPDATE") { 207 | var issueUpdate = function(err, value) { 208 | if (err) { 209 | cb(err); 210 | } else { 211 | if (action === "CREATE") { 212 | createRecordSet(hostedZoneId, params.recordSetName, params.type || "CNAME", value, params.ttl || 60, cb); 213 | } else if (action === "UPDATE") { 214 | updateRecordSet(hostedZoneId, params.recordSetName, params.type || "CNAME", value, params.ttl || 60, cb); 215 | } 216 | } 217 | }; 218 | if (params.iface){ 219 | var ifaces = os.networkInterfaces(); 220 | var iface = ifaces[params.iface]; 221 | if (iface === undefined) { 222 | cb(new Error("interface not found")); 223 | } else { 224 | assert.arrayOfObject(iface, "iface present"); 225 | var ipv4 = underscore.find(iface, function(binding) { return binding.family === "IPv4"; }); 226 | if (ipv4 === undefined) { 227 | cb(new Error("interface has no IPv4 address")); 228 | } else { 229 | issueUpdate(null, ipv4.address); 230 | } 231 | } 232 | } else { 233 | var mds = new AWS.MetadataService(); 234 | mds.request("/latest/meta-data/" + (params.metadata || "public-hostname"), issueUpdate); 235 | } 236 | } else if (action === "DELETE") { 237 | deleteRecordSet(hostedZoneId, params.recordSetName, cb); 238 | } else { 239 | cb(new Error("action must be one of CREATE, UPDATE, or DELETE")); 240 | } 241 | } 242 | 243 | function input(action, params, cb) { 244 | "use strict"; 245 | assert.string(action, "action"); 246 | assert.optionalString(params.hostedZoneId, "params.hostedZoneId"); 247 | assert.string(params.recordSetName, "params.recordSetName"); 248 | assert.optionalNumber(params.ttl, "params.ttl"); 249 | assert.optionalString(params.metadata, "params.metadata"); 250 | assert.optionalString(params.type, "params.type"); 251 | assert.optionalString(params.iface, "params.iface"); 252 | assert.func(cb, "cb"); 253 | if (params.hostedZoneId !== undefined) { 254 | run(action, params.hostedZoneId, params, cb); 255 | } else { 256 | assert.string(params.hostedZoneName, "params.hostedZoneName"); 257 | retrieveHostedZoneId(params.hostedZoneName, function(err, hostedZoneId) { 258 | if (err) { 259 | cb(err); 260 | } else { 261 | run(action, hostedZoneId, params, cb); 262 | } 263 | }); 264 | } 265 | } 266 | 267 | module.exports = input; 268 | exports.retrieveHostedZoneId = retrieveHostedZoneId; 269 | exports.retrieveRecordSet = retrieveRecordSet; 270 | exports.deleteRecordSet = deleteRecordSet; 271 | exports.createRecordSet = createRecordSet; 272 | exports.updateRecordSet = updateRecordSet; 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "route53-updater", 3 | "version": "1.0.1", 4 | "description": "Updating a Route53 resource set with meta-data of EC2 instance", 5 | "keywords": ["Route53", "AWS"], 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "make test" 9 | }, 10 | "author": "Michael Wittig ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "assert-plus": "1.0.0", 14 | "aws-sdk": "2.4.1", 15 | "async": "1.5.2", 16 | "underscore": "1.8.3", 17 | "minimist": "1.2.0" 18 | }, 19 | "devDependencies": { 20 | "jshint": "2.9.2", 21 | "madge": "0.5.4", 22 | "npmedge": "0.2.2", 23 | "istanbul": "0.4.4" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/widdix/node-route53-updater.git" 28 | }, 29 | "bin" : { 30 | "route53-updater": "./cli.js" 31 | } 32 | } 33 | --------------------------------------------------------------------------------