Comment monitorer de façon habituelle une base de données Oracle / AWS RDS?

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.

 

— 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.
 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *