En début d’année, un de nos clients nous a sollicités après avoir finalisé un projet de replatforming d’une partie de ses bases de données ORACLE, vers le service AWS nommé RDS (Relational Database Service).
Le souhait de notre client était de pouvoir continuer à surveiller ses bases de données avec sa solution standard et normalisée, à savoir, un NAGIOS On Premise. L’objectif étant de rendre le plus transparent possible, dans le cadre du monitoring quotidien, du fait que certaines de ses bases de données se trouvent désormais dans « le nuage AWS ».
Ce bref article a pour vocation de vous présenter la solution proposée, retenue, et d’ailleurs toujours utilisée.
— Présentation de AWR RDS
Amazon Relational Database Service (Amazon RDS) permet de provisionner et de gérer aisément une base de données relationnelle dans le cloud AWS.
Amazon RDS est disponible sur plusieurs types d’instances de base de données (optimisées pour la mémoire, les performances ou les I/O). Ce service donne le choix entre six moteurs de base de données notamment Oracle, Amazon Aurora, PostgreSQL, MySQL, MariaDB et Microsoft SQL Server.
Un service spécifique (AWS Database Migration Service) permet de migrer ou répliquer rapidement ses bases de données existantes, vers le service Amazon RDS.
— Présentation de Amazon CloudWatch
Amazon CloudWatch est un service de surveillance pour les ressources du cloud AWS et notamment, puisque que c’est cela qui nous intéresse ici, pour les instances Amazon RDS DB.
Amazon CloudWatch permet, entre autres, de collecter et de suivre de nombreuses métriques.
En nous renseignant sur ce service, nous nous sommes rendus compte de l’existence d’un nombre conséquent d’API(s) proposées nativement, ainsi que de scripts disponibles dans les dépôts publics (GitHub etc ..). A partir de là, la réponse à proposer à notre client était pratiquement toute faite.
En nous renseignant sur ce service, nous nous sommes rendus compte de l’existence d’un nombre conséquent d’API(s) proposées nativement, ainsi que de scripts disponibles dans les dépôts publics (GitHub etc ..). A partir de là, la réponse à proposer à notre client était pratiquement toute faite.
— Solution proposée
Voici la dernière itération du script Python que nous avons utilisé :
#!/usr/bin/python import argparse, logging, nagiosplugin from boto.ec2 import cloudwatch from datetime import datetime, timedelta class CloudWatchBase(nagiosplugin.Resource): def __init__(self, namespace, metric, dimensions, statistic, period, lag, region=None): self.namespace = namespace self.metric = metric self.dimensions = dimensions self.statistic = statistic self.period = int(period) self.lag = int(lag) if region: self.region = region else: self.region = cloudwatch.CloudWatchConnection.DefaultRegionName def _connect(self): try: self._cw except AttributeError: self._cw = cloudwatch.connect_to_region(self.region) return self._cw class CloudWatchMetric(CloudWatchBase): def probe(self): logging.info('getting stats from cloudwatch') cw = self._connect() start_time = datetime.utcnow() - timedelta(seconds=self.period) - timedelta(seconds=self.lag) logging.info(start_time) end_time = datetime.utcnow() stats = [] stats = cw.get_metric_statistics(self.period, start_time, end_time, self.metric, self.namespace, self.statistic, self.dimensions) if len(stats) == 0: return [] stat = stats[0] return [nagiosplugin.Metric('cloudwatchmetric', stat[self.statistic], stat['Unit'])] class CloudWatchRatioMetric(nagiosplugin.Resource): def __init__(self, dividend_namespace, dividend_metric, dividend_dimension, dividend_statistic, period, lag, divisor_namespace, divisor_metric, divisor_dimension, divisor_statistic, region): self.dividend_metric = CloudWatchMetric(dividend_namespace, dividend_metric, dividend_dimension, dividend_statistic, int(period), int(lag), region) self.divisor_metric = CloudWatchMetric(divisor_namespace, divisor_metric, divisor_dimension, divisor_statistic, int(period), int(lag), region) def probe(self): dividend = self.dividend_metric.probe()[0] divisor = self.divisor_metric.probe()[0] ratio_unit = '%s / %s' % ( dividend.uom, divisor.uom) return [nagiosplugin.Metric('cloudwatchmetric', dividend.value / divisor.value, ratio_unit)] class CloudWatchDeltaMetric(CloudWatchBase): def __init__(self, namespace, metric, dimensions, statistic, period, lag, delta, region): super(CloudWatchDeltaMetric, self).__init__(namespace, metric, dimensions, statistic, period, lag, region) self.delta = delta def probe(self): logging.info('getting stats from cloudwatch') cw = self._connect() datapoint1_start_time = (datetime.utcnow() - timedelta(seconds=self.period) - timedelta(seconds=self.lag)) - timedelta(seconds=self.delta) datapoint1_end_time = datetime.utcnow() - timedelta(seconds=self.delta) datapoint1_stats = cw.get_metric_statistics(self.period, datapoint1_start_time, datapoint1_end_time, self.metric, self.namespace, self.statistic, self.dimensions) datapoint2_start_time = datetime.utcnow() - timedelta(seconds=self.period) - timedelta(seconds=self.lag) datapoint2_end_time = datetime.utcnow() datapoint2_stats = cw.get_metric_statistics(self.period, datapoint2_start_time, datapoint2_end_time, self.metric, self.namespace, self.statistic, self.dimensions) if len(datapoint1_stats) == 0 or len(datapoint2_stats) == 0: return [] datapoint1_stat = datapoint1_stats[0] datapoint2_stat = datapoint2_stats[0] num_delta = datapoint2_stat[self.statistic] - datapoint1_stat[self.statistic] per_delta = (100 / datapoint2_stat[self.statistic]) * num_delta return [nagiosplugin.Metric('cloudwatchmetric', per_delta, '%')] class CloudWatchMetricSummary(nagiosplugin.Summary): def __init__(self, namespace, metric, dimensions, statistic): self.namespace = namespace self.metric = metric self.dimensions = dimensions self.statistic = statistic def ok(self, results): full_metric = '%s:%s' % (self.namespace, self.metric) return 'CloudWatch Metric %s with dimensions %s' % (full_metric, self.dimensions) def problem(self, results): full_metric = '%s:%s' % (self.namespace, self.metric) return 'CloudWatch Metric %s with dimensions %s' % (full_metric, self.dimensions) class CloudWatchMetricRatioSummary(nagiosplugin.Summary): def __init__(self, dividend_namespace, dividend_metric, dividend_dimensions, dividend_statistic, divisor_namespace, divisor_metric, divisor_dimensions, divisor_statistic): self.dividend_namespace = dividend_namespace self.dividend_metric = dividend_metric self.dividend_dimensions = dividend_dimensions self.dividend_statistic = dividend_statistic self.divisor_namespace = divisor_namespace self.divisor_metric = divisor_metric self.divisor_dimensions = divisor_dimensions self.divisor_statistic = divisor_statistic def ok(self, results): dividend_full_metric = '%s:%s' % (self.dividend_namespace, self.dividend_metric) divisor_full_metric = '%s:%s' % (self.divisor_namespace, self.divisor_metric) return 'Ratio: CloudWatch Metric %s with dimensions %s / CloudWatch Metric %s with dimensions %s' % (dividend_full_metric, self.dividend_dimensions, divisor_full_metric, self.divisor_dimensions) def problem(self, results): dividend_full_metric = '%s:%s' % (self.dividend_namespace, self.dividend_metric) divisor_full_metric = '%s:%s' % (self.divisor_namespace, self.divisor_metric) return 'Ratio: CloudWatch Metric %s with dimensions %s / CloudWatch Metric %s with dimensions %s' % (dividend_full_metric, self.dividend_dimensions, divisor_full_metric, self.divisor_dimensions) class CloudWatchDeltaMetricSummary(nagiosplugin.Summary): def __init__(self, namespace, metric, dimensions, statistic, delta): self.namespace = namespace self.metric = metric self.dimensions = dimensions self.statistic = statistic self.delta = delta def ok(self, results): full_metric = '%s:%s' % (self.namespace, self.metric) return 'CloudWatch %d seconds Delta %s Metric with dimensions %s' % (self.delta, full_metric, self.dimensions) def problem(self, results): full_metric = '%s:%s' % (self.namespace, self.metric) return 'CloudWatch %d seconds Delta %s Metric with dimensions %s' % (self.delta, full_metric, self.dimensions) class KeyValArgs(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): kvs = {} for pair in values.split(','): kv = pair.split('=') kvs[kv[0]] = kv[1] setattr(namespace, self.dest, kvs) @nagiosplugin.guarded def main(): argp = argparse.ArgumentParser(description='Nagios plugin to check cloudwatch metrics') argp.add_argument('-n', '--namespace', required=True, help='namespace for cloudwatch metric') argp.add_argument('-m', '--metric', required=True, help='metric name') argp.add_argument('-d', '--dimensions', action=KeyValArgs, help='dimensions of cloudwatch metric in the format dimension=value[,dimension=value...]') argp.add_argument('-s', '--statistic', choices=['Average','Sum','SampleCount','Maximum','Minimum'], default='Average', help='statistic used to evaluate metric') argp.add_argument('-p', '--period', default=60, type=int, help='the period in seconds over which the statistic is applied') argp.add_argument('-l', '--lag', default=0, help='delay in seconds to add to starting time for gathering metric. useful for ec2 basic monitoring which aggregates over 5min periods') argp.add_argument('-r', '--ratio', default=False, action='store_true', help='this activates ratio mode') argp.add_argument('--divisor-namespace', help='ratio mode: namespace for cloudwatch metric of the divisor') argp.add_argument('--divisor-metric', help='ratio mode: metric name of the divisor') argp.add_argument('--divisor-dimensions', action=KeyValArgs, help='ratio mode: dimensions of cloudwatch metric of the divisor') argp.add_argument('--divisor-statistic', choices=['Average','Sum','SampleCount','Maximum','Minimum'], help='ratio mode: statistic used to evaluate metric of the divisor') argp.add_argument('--delta', type=int, help='time in seconds to build a delta mesurement') argp.add_argument('-w', '--warning', metavar='RANGE', default=0, help='warning if threshold is outside RANGE') argp.add_argument('-c', '--critical', metavar='RANGE', default=0, help='critical if threshold is outside RANGE') argp.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity (use up to 3 times)') argp.add_argument('-R', '--region', help='The AWS region to read metrics from') args=argp.parse_args() if args.ratio: metric = CloudWatchRatioMetric(args.namespace, args.metric, args.dimensions, args.statistic, args.period, args.lag, args.divisor_namespace, args.divisor_metric, args.divisor_dimensions, args.divisor_statistic, args.region) summary = CloudWatchMetricRatioSummary(args.namespace, args.metric, args.dimensions, args.statistic, args.divisor_namespace, args.divisor_metric, args.divisor_dimensions, args.divisor_statistic) elif args.delta: metric = CloudWatchDeltaMetric(args.namespace, args.metric, args.dimensions, args.statistic, args.period, args.lag, args.delta, args.region) summary = CloudWatchDeltaMetricSummary(args.namespace, args.metric, args.dimensions, args.statistic, args.delta) else: metric = CloudWatchMetric(args.namespace, args.metric, args.dimensions, args.statistic, args.period, args.lag, args.region) summary = CloudWatchMetricSummary(args.namespace, args.metric, args.dimensions, args.statistic) check = nagiosplugin.Check( metric, nagiosplugin.ScalarContext('cloudwatchmetric', args.warning, args.critical), summary) check.main(verbose=args.verbose) if __name__ == "__main__": main()
Ci-dessous, un exemple d’exécution du script, depuis le serveur NAGIOS On Premise de notre client :
/usr/bin/check_cloudwatch.py -R eu-west-1 -n AWS/RDS -m DatabaseConnections -p 300 -d DBInstanceIdentifier=my_database
Ici, on interroge la métrique « CloudWatch » nommée « DatabaseConnections » et relative à la base RDS nommée « my_database ».
La liste exhaustive et évolutive des métriques « CloudWatch » est disponible dans la documentation AWS :
Afin de sécuriser le tout, les credentials d’accès au service ont été stockés directement dans le .bash_profile du compte OS qui exécutait le script.
Une fois le bon fonctionnement de ce script éprouvé, il ne suffisait alors plus que de l’intégrer en tant que plugin dans NAGIOS, et le tour était joué :
Grâce à cette solution, les équipes opérationnelles en charge du monitoring des multiples bases de données du site n’ont pas à faire la distinction, au quotidien, entre les environnements On Premise et les environnements présents dans le Cloud AWS.