Skip to content

Instantly share code, notes, and snippets.

@etyp
Last active April 9, 2018 23:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save etyp/24c2cf1a77c77f98d1ed2edaf7d5c96f to your computer and use it in GitHub Desktop.
Save etyp/24c2cf1a77c77f98d1ed2edaf7d5c96f to your computer and use it in GitHub Desktop.
Sample files for subscribing to SNS and starting graceful shutdown on given AWS instance using mongo (since load balancer will not route predictably)
import xml2js from 'xml2js';
import AWS from 'aws-sdk';
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import { Picker } from 'meteor/meteorhacks:picker';
import { DeathrowInstances } from '/imports/startup/server/ddp-graceful-shutdown';
import { errorResponse } from '/imports/api/our-api/server/route-helpers.js';
import './middleware';
const SNS_PROTOCOL = Meteor.isDev() ? 'http' : 'https';
const SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:1234567891234:test-ebs-graceful-shutdown'; // Your topic ARN goes here.
const SNS_PATH = '__aws/sns';
const SNS_ENDPOINT = Meteor.isDev() ? `http://some.ngrok.url/${SNS_PATH}` : Meteor.absoluteUrl(SNS_PATH);
const { AWSAccessKeyId, AWSSecretAccessKey } = Meteor.settings.AWS;
AWS.config.update({ accessKeyId: AWSAccessKeyId, secretAccessKey: AWSSecretAccessKey, region: 'us-east-1' });
const sns = new AWS.SNS();
sns.subscribe(
{
TopicArn: SNS_TOPIC_ARN,
Protocol: SNS_PROTOCOL,
Endpoint: SNS_ENDPOINT,
},
(error, data) => {
if (error) throw new Meteor.Error(`Unable to set up SNS subscription: ${error}`);
console.log(`SNS subscription set up successfully: ${JSON.stringify(data)}`);
}
);
// On new SNS notification, check if the lifecycle transition
// is TERMINATING. If so, insert a doc to Deathrow collection
// so other server instances know to call graceful shutdown if
// the inserted doc matches the AWS instance id.
const handleSnsNotification = (params, req) => {
const message = JSON.parse(req.body.Message);
if (message.detail) {
const instanceId = message.detail.EC2InstanceId;
const lifecycleTransition = message.detail.LifecycleTransition;
if (instanceId) console.log(`SNS Notification has instance id: ${instanceId}`);
if (lifecycleTransition) console.log(`SNS notification has lifecycle transition: ${lifecycleTransition}`);
if (lifecycleTransition === 'autoscaling:EC2_INSTANCE_TERMINATING') {
console.log(`Need to do graceful shutdown on instance ${instanceId}`);
const docId = DeathrowInstances.insert({ instanceId });
console.log(`Inserted deathrow shutdown with _id ${docId}`);
}
}
};
// On new SNS subscription confirmation, visit the URL provided
// and use AWS SNS sdk to confirm the subscription.
const handleSnsSubscriptionConfirmation = (params, req) => {
const { Token, SubscribeURL } = req.body;
HTTP.call('GET', SubscribeURL, (error, result) => {
if (error) throw new Meteor.Error(`Erorr getting SNS subscription URL: ${error}`);
const parser = new xml2js.Parser();
parser.parseString(result.content, (parseError, doc) => {
if (parseError) throw new Meteor.Error(`Erorr parsing SNS subscription URL contents: ${parseError}`);
const subscriptionArn = doc.ConfirmSubscriptionResponse.ConfirmSubscriptionResult[0].SubscriptionArn[0];
if (subscriptionArn.includes(SNS_TOPIC_ARN)) {
console.log(`Confirming subscription with ARN ${subscriptionArn}...`);
sns.confirmSubscription(
{
TopicArn: SNS_TOPIC_ARN,
Token,
},
(confirmError, data) => {
if (confirmError) throw new Meteor.Error(`Unable to confirm SNS subscription: ${confirmError}`);
console.log(`SNS subscription confirmed: ${JSON.stringify(data)}`);
}
);
} else {
console.log(
`Subscription ARN ${subscriptionArn} does not match SNS Topic ARN ${SNS_TOPIC_ARN} |
Ignoring subscription for now.`
);
}
});
});
};
// Catch all AWS SNS messages here.
Picker.route(`/${SNS_PATH}`, (params, req, res) => {
try {
if (req.headers['x-amz-sns-message-type'] === 'Notification' && req.body.Message) {
handleSnsNotification(params, req, res);
} else if (req.headers['x-amz-sns-message-type'] === 'SubscriptionConfirmation') {
handleSnsSubscriptionConfirmation(params, req, res);
}
res.writeHead(200, { message: 'Okay' });
res.end();
} catch (error) {
errorResponse(res, error);
}
});
import AWS from 'aws-sdk';
import { DDPGracefulShutdown } from '@meteorjs/ddp-graceful-shutdown';
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
export const DeathrowInstances = new Mongo.Collection('deathrowInstances');
const shutdownHandler = new DDPGracefulShutdown({
gracePeriodMillis: 1000 * process.env.METEOR_SIGTERM_GRACE_PERIOD_SECONDS,
server: Meteor.server,
});
// Set up observer on DeathrowInstances collection. If a new doc is added
// and the `instanceId` matches the AWS instance ID of the currently running
// box, we want to close connections on the box and clear the new document
// from the collection.
DeathrowInstances.find({}).observe({
added(doc) {
console.log(`Found doc with instance id ${doc.instanceId}`);
if (doc.instanceId === Meteor.getHostname()) {
// Shut self down!
console.log('Matches current instance. Shutting self down.');
Meteor.defer(() => {
DeathrowInstances.remove({ _id: doc._id });
});
shutdownHandler.closeConnections({ log: true });
} else {
console.log(`Instance ${Meteor.getHostname()} (self) does not match doc instance ${doc.instanceId}`);
}
},
});
import os from 'os';
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
Meteor.getHostname = () => {
let hostname = os.hostname();
try {
// If you do a GET on EC2 instances, you can get the instance ID.
// See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
const response = HTTP.get('http://169.254.169.254/latest/meta-data/instance-id');
hostname = response.content;
} catch (instanceIdGetError) {
console.log('Error getting AWS instance ID. Using OS hostname for now.');
}
return hostname;
};
// Set up all Picker middleware here so we don't have duplicate middleware
// and ensure they're all added in same place.
import BodyParser from 'body-parser';
import { Picker } from 'meteor/meteorhacks:picker';
// Ensure all AWS SNS messages get parsed as JSON instead of plaintext.
Picker.middleware((req, res, next) => {
if (req.headers['x-amz-sns-message-type']) {
req.headers['content-type'] = 'application/json;charset=UTF-8';
}
next();
});
Picker.middleware(BodyParser.json());
Picker.middleware(BodyParser.urlencoded({ extended: false }));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment