AWS - En route vers la Production” - Partie 3/3 - Les Sauvegardes & La maintenance

Un autre domaine dans lequel AWS fournit des outils clé en main est le domaine des sauvegardes et plus généralement la possibilité de scripter vos traitements quotidiens.

5.1 Sauvegarder ses EBS

Les sauvegardes dans AWS ne sont pas abordées sous la forme classique des sauvegardes comme l’entend une équipe de production. Sur AWS, on parle plutôt de “snapshots”.
Les données de vos machines sont conservées sur des EBS (Elastic Block Store). Ces EBS peuvent être “snapshotés” à tout moment. Néanmoins, un snapshot vous permettra de restaurer l’intégralité du filesystem se trouvant sur l’EBS mais pas un fichier en particulier.
C’est en çà que cela n’est pas assimilable à des sauvegardes. Une fois ce point précisé, on peut quand même utiliser les snapshots des EBS, c’est déjà un minimum.

Pour des sauvegardes sur des objet ciblés (EBS, Tables d’une base de données), AWS vous propose “AWS Backup”. Cet outil est utilisable depuis la console ou par appel sur l’API. Je vous invite à lire l’article suivant pour un rapide tour des fonctionnalités offertes par l’outil :
https://aws.amazon.com/blogs/aws/aws-backup-automate-and-centrally-manage-your-backups

Je vous joins également ce white paper écrit par AWS sur les différentes approches pour vos sauvegardes, dans des contextes “pur Cloud”, “On-Premise” ou “Hybrid” :
https://d1.awsstatic.com/whitepapers/Backup_and_Recovery_Approaches_Using_AWS.pdf

Dans ce billet, pour une première approche, je vous propose d’automatiser les snapshots de nos disques EBS. Pour cela, l’idée est de poser des tags sur les EBS. Ces tags fourniront de l’information qui sera utilisée par les fonctions lambda :
“Backup”. Si la valeur = “Yes”, l’EBS doit être snapshoté
“Retention” : nombre de jours à conserver.

Le script terraform sera :

#######################
# Creation du role
#######################
resource "aws_iam_role" "lambda" {
  name = "${var.aws_prefix}-role-lambda-snapshot"
  description = "Allows Lambda Function to call Snapshot EBS"
  path        = "/"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

#######################
# Creation du profil associé au role
#######################
resource "aws_iam_instance_profile" "lambda" {
  name = "${var.aws_prefix}-profile-lambda-snapshot"
  role = "${aws_iam_role.lambda.id}"
}

#######################
# Creation de la policy pour la fonction lambda
#######################
resource "aws_iam_policy" "lambda" {
  name        = "${var.aws_prefix}-policy-lambda-snapshot"
  path        = "/"
  description = "Access for Lambda to Bucket for backup"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:*"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": "ec2:Describe*",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSnapshot",
                "ec2:DeleteSnapshot",
                "ec2:CreateTags",
                "ec2:ModifySnapshotAttribute",
                "ec2:ResetSnapshotAttribute"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
EOF
}

#######################
# On attache la policy au role
#######################
resource "aws_iam_policy_attachment" "lambda-attach" {
  name       = "${var.aws_prefix}-lambda-snapshot-attachment"
  roles      = ["${aws_iam_role.lambda.name}"]
  policy_arn = "${aws_iam_policy.lambda.arn}"
}

#######################
# Creation de la fonction lambda "create-snapshots"
#######################
resource "aws_lambda_function" "create-snapshots" {
  role          = "${aws_iam_role.lambda.arn}"
  handler       = "ebs-create-snapshots.lambda_handler"
  runtime       = "python2.7"
  filename      = "ebs-create-snapshots.zip"
  function_name = "${var.aws_prefix}-ebs-create-snapshots"
  source_code_hash = "${base64sha256(file("ebs-create-snapshots.zip"))}"
  description   = "Lambda for create EBS snapshots"
}

#######################
# Creation de la fonction lambda "remove-snapshots"
#######################
resource "aws_lambda_function" "remove-snapshots" {
  role          = "${aws_iam_role.lambda.arn}"
  handler       = "ebs-remove-snapshots.lambda_handler"
  runtime       = "python2.7"
  filename      = "ebs-remove-snapshots.zip"
  function_name = "${var.aws_prefix}-ebs-remove-snapshots"
  source_code_hash = "${base64sha256(file("ebs-remove-snapshots.zip"))}"
  description   = "Lambda for remove EBS snapshots"
}

#######################
# Creation de l'entrée de la cron cloudWatch "0 1 * * ? *"
#######################
resource "aws_cloudwatch_event_rule" "lambda" {
  name        = "lambda_schedule_snap"
  description = "Lauch snapshot at 01:00"
  schedule_expression = "cron(0 1 * * ? *)"
}


#######################
# Creation de la permission associée 
# pour l'execution de la fonction lambda "remove-snapshots"
#######################
resource "aws_lambda_permission" "lambda_cron_snap_remove" {
  statement_id = "AllowExecutionFromCloudWatch"
  action = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.remove-snapshots.function_name}"
  principal = "events.amazonaws.com"
  source_arn = "${aws_cloudwatch_event_rule.lambda.arn}"
}

#######################
# Creation de la permission associée 
# pour l'execution de la fonction lambda "create-snapshots"
#######################
resource "aws_lambda_permission" "lambda_cron_snap_create" {
  statement_id = "AllowExecutionFromCloudWatch"
  action = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.create-snapshots.function_name}"
  principal = "events.amazonaws.com"
  source_arn = "${aws_cloudwatch_event_rule.lambda.arn}"
}

#######################
# Execution de la lambda "remove-snapshots"
# tous les jours à 1 Heure
#######################
resource "aws_cloudwatch_event_target" "lambda_remove" {
  rule      = "${aws_cloudwatch_event_rule.lambda.name}"
  arn       = "${aws_lambda_function.remove-snapshots.arn}"
}

#######################
# Execution de la lambda "create-snapshots"
# tous les jours à 1 Heure
#######################
resource "aws_cloudwatch_event_target" "lambda_create" {
  rule      = "${aws_cloudwatch_event_rule.lambda.name}"
  arn       = "${aws_lambda_function.create-snapshots.arn}"
}

Enfin, et voici le code de la fonction lambda de création de snapshots :

import boto3
import collections
import datetime

region = 'eu-west-3'    # region we're running in (should be changed to be auto-determined 

ec = boto3.client('ec2')

def lambda_handler(event, context):
    reservations = ec.describe_instances(
        Filters=[
            { 'Name': 'tag:Backup', 'Values': ['Yes'] },
        ]
    ).get(
        'Reservations', []
    )

    instances = sum(
        [
            [i for i in r['Instances']]
            for r in reservations
        ], [])

    print "Found %d instances that need backing up" % len(instances)

    to_tag = collections.defaultdict(list)

    for instance in instances:
        try:
            retention_days = [
                int(t.get('Value')) for t in instance['Tags']
                if t['Key'] == 'Retention'][0]
        except IndexError:
            retention_days = 7

        for dev in instance['BlockDeviceMappings']:
            if dev.get('Ebs', None) is None:
                continue
            vol_id = dev['Ebs']['VolumeId']
            dev_name = dev['DeviceName']
            print "\tFound EBS volume %s (%s) on instance %s" % (
                vol_id, dev_name, instance['InstanceId'])

            volume = ec.describe_volumes(
                    VolumeIds=[vol_id]
                )
                
            try:
                backup_vol = [
                    t.get('Value') for t in volume['Volumes'][0]['Tags']
                    if t['Key'] == 'Backup'][0]
            except IndexError:
                backup_vol = "Yes"

            if backup_vol == "No":
                break

            # figure out instance name if there is one
            instance_name = ""
            for tag in instance['Tags']:
                if tag['Key'] != 'Name':
                    continue
                else:
                    instance_name = tag['Value']
            
            description = '%s - %s (%s)' % ( instance_name, vol_id, dev_name )

            # trigger snapshot
            snap = ec.create_snapshot(
                VolumeId=vol_id, 
                Description=description
                )
            
            if (snap):
                print "\t\tSnapshot %s created in %s of [%s]" % ( snap['SnapshotId'], region, description )
            to_tag[retention_days].append(snap['SnapshotId'])
            print "\t\tRetaining snapshot %s of volume %s from instance %s (%s) for %d days" % (
                snap['SnapshotId'],
                vol_id,
                instance['InstanceId'],
                instance_name,
                retention_days,
            )

    for retention_days in to_tag.keys():
        delete_date = datetime.date.today() + datetime.timedelta(days=retention_days)
        delete_fmt = delete_date.strftime('%Y-%m-%d')
        print "Will delete %d snapshots on %s" % (len(to_tag[retention_days]), delete_fmt)
        ec.create_tags(
            Resources=to_tag[retention_days],
            Tags=[
                { 'Key': 'DeleteOn', 'Value': delete_fmt },
                { 'Key': 'Type', 'Value': 'Automated' },
            ]
)

5.2 Database Recovery Point In Time

Je ne pouvais pas, dans ce chapitre, ne pas parler des sauvegardes de base de données. Un snapshot quotidien est programmé par défaut. Bien évidemment, rien ne vous empêche de scripter un cron qui viendra exécuter un dump de vos données.
Je voulais plutôt attirer votre attention sur une fonctionnalité native d’AWS sur les bases de données Aurora : la possibilité de repartir d’une base à partir des “Recovery Point In Time”, aussi bien sur vos bases mono-AZ que multi-AZ (cluster).
C’est détaillé ici :
https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_PIT.html

Remarquez qu’il est possible de lancer un recovery de plusieurs manières (depuis la console, depuis l’api). Lancer une opération de recovery viendra recréer un cluster ou une instance à côté.
Pour réduire le temps d’indisponibilité par rapport à la propagation du nouveau nom DNS de votre base restaurée, vous pouvez utiliser Route53 et ajouter un domaine interne. Par exemple myproject.io. Dans ce domaine, il suffira de déclarer une entrée “mydatabase.myproject.io” de type CNAME sur le nom de la base que vous souhaitez. Le paramétrage de l’applicatif pointera sur ce nom et non sur le DNS “amazonaws.com”.

6. La maintenance globale avec AWS System Manager (SSM)

Lors d’un bootcamp chez AWS, j’avais assisté à la démonstration de SSM pour “AWS System Manager”.
Sur un parc de 200 machines (moitié Windows, moitié Linux), il nous montrait comment il appliquait des patchs. Assez bluffant, en comparaison avec d’autres systèmes de ce type que j’avais pu rencontré dans mes expériences passées.

“SSM” est accessible dans la console, directement dans l’écran EC2 :

blog2-ssm-console

Pour fonctionner, il est nécessaire que les profils de vos machines EC2 contiennent la policy suivante : AmazonEC2RoleforSSM

Il sera également nécessaire de tagger vos VMs avec des valeurs qui permettront de les filtrer.

blog2-ssm-rc

Sélectionner les machines sur lesquelles vous souhaitez lancer votre commande (soit en les cochant, soit en les sélectionnant via une valeur de tag) :

blog2-ssm-rc2

Une fois validé, la commande est lancée sur l’ensemble de vos machines. Dans le chapitre consacré à CloudWatch, on peut lancer une installation de l’agent CloudWatch via ssm. Dans la liste des “Command Document” vous pouvez utiliser :
AmazonInspector-ManageAWSAgent
AmazonCloudWatch-MigrateCloudWatchAgent
AmazonCloudWatch-ManageAgent
AWS-ConfigureAWSPackage

Bien évidemment, il sera possible de programmer vos jobs. Voici un article de blog sur le sujet.
https://aws.amazon.com/fr/blogs/mt/automate-running-tasks-using-amazon-ec2-systems-manager-maintenance-windows/

Je conclurai ici cette présentation de SSM sur la sécurité. L’accès à SSM doit être contrôlé. Il est nécessaire de segmenter les populations d’utilisateurs qui se connectent à votre console. D’autre part, la possibilité de lancer “en masse” des commandes sur votre infrastructure nécessite de tester et maîtriser vos scripts ! “de grands pouvoirs impliquent de grandes responsabilités” comme on dit dans les équipes de prod !!

Dernier détail : depuis Septembre 2018, AWS a ajouté une fonctionnalité dans “SSM” : Session Start. Ceci permet, moyennant les policy posées correctement sur les profils de vos machines EC2, de prendre la main sur vos machines. C’est détaillé ici :
https://aws.amazon.com/fr/blogs/aws/new-session-manager/

7. Conclusion

Les différentes techniques présentées dans ce billet vous permettront d’aborder dans un premier temps votre nouvelle infrastructure. Au delà des scripts et des techniques mises en œuvre, je voulais attirer votre attention sur le fait qu’AWS fournit de nombreux outils matures pour votre production. Une compétence “python” dans vos équipes vous sera très utile si vous souhaitez utiliser pleinement les possibilités des lambdas.
Je n’ai évidemment pas été exhaustif. Voici une liste d’autres points qui peuvent vous intéresser :
AWS Inspector : Pour scanner les vulnérabilités de vos systèmes d’exploitation et des composants installés sur vos machines.
X-Ray : Dans une architecture micro-service ou serverless, analyser et tracer vos appels entre vos services.
AWS Config : Cela vous permettra de mettre en place un audit de votre infrastructure selon les règles que vous aurez définies.

Sur les sujets du monitoring et de l’indexation des logs, il existe pléthore d’outils (payants) vous permettant de vous affranchir de beaucoup de problématiques. Citons-en 2 :
splunk
datadog

Ces produits peuvent être utilisés en mode “SaaS” ou directement installés dans un VPC. Notons également que d’autres produits utilisent une approche différente : basés sur le “service discovery”, ces outils vous permettront de mettre en place du monitoring / Alerting sur une infrastructure déjà en place. Par exemple, Instana ou Ruxit.

Vous utilisez Puppet ? AWS propose AWS Ops Works. C’est un service managé d’un outil de “Maintien en condition opérationnelle”,comme Puppet ou Chef. Pour les services qui ont investi du temps dans l’écriture de modules puppet, cela peut faciliter grandement la mise en œuvre.

La sécurité est un domaine extrêmement important pour vous ? AWS propose KMS. Plus de besoin de monter un cluster Vault, KMS apporte toutes les fonctionnalités nécessaires pour augmenter drastiquement la sécurité de vos données, de vos traitements (rotation des clés, chiffrement de vos EBS, gestion des secrets etc …). Les bases de données Aurora & DynamoDB supportent la rotation des clés.

Pour conclure, en fonction de votre contexte et des différents use-cases qui s’offrent à vous, vous pourrez aborder votre transformation cloud selon plusieurs principes technologiques. Je vous invite à lire notre livre blanc sur le sujet des “Well Architecture Framework” pour une description détaillée de ces uses-cases.