web-dev-qa-db-fra.com

Créer une image AMI dans le cadre d'une pile de cloudformation

Je souhaite créer une pile de cloudformation EC2 qui peut être décrite dans les étapes suivantes:

1.- Instance de lancement

2.- Provisionner l'instance

3.- Arrêtez l'instance et créez une image AMI

4.- Créez un groupe de mise à l'échelle automatique avec l'image AMI créée comme source pour lancer de nouvelles instances.

Fondamentalement, je peux faire 1 et 2 dans un modèle de cloudformation et 4 dans un deuxième modèle. Ce que je ne semble pas en mesure de faire, c'est de créer une image AMI à partir d'une instance dans un modèle cloudformation, ce qui engendre le problème de devoir supprimer manuellement l'AMI si je veux supprimer la pile.

Cela étant dit, mes questions sont les suivantes:

1.- Y a-t-il un moyen de créer une image AMI à partir d'une instance DANS le modèle cloudformation?

2.- Si la réponse à 1 est non, existe-t-il un moyen d'ajouter une image AMI (ou toute autre ressource) pour l'intégrer à une pile terminée?

MODIFIER:

Juste pour clarifier les choses, j'ai déjà résolu le problème de la création de l'AMI et de son utilisation dans un modèle cloudformation. Je ne peux tout simplement pas créer l'AMI DANS le modèle cloudformation ni l'ajouter d'une manière ou d'une autre à la pile créée.

Comme je l'ai commenté sur la réponse de Rico, ce que je fais maintenant, c'est d'utiliser un livre de jeu ansible qui comporte essentiellement 3 étapes:

1.- Créer une instance de base avec un modèle cloudformation

2.- Créer, en utilisant ansible, une AMI de l'instance créée à l'étape 1

3.- Créez le reste de la pile (ELB, groupes de mise à l'échelle automatique, etc.) avec un deuxième modèle de cloudformation qui met à jour celui créé à l'étape 1 et qui utilise l'AMI créée à l'étape 2 pour lancer des instances.

Voici comment je le gère maintenant, mais je voulais savoir s’il était possible de créer une AMI DANS un modèle de cloudformation ou s’il était possible d’ajouter l’AMI créée à la pile (quelque chose comme dire à la pile, "Hé, cela appartient à vous aussi, alors manipulez-le ").

20
user2422451

Oui, vous pouvez créer une AMI à partir d'une instance EC2 dans un modèle CloudFormation en implémentant un Ressource personnalisée qui appelle le CreateImage API lors de la création (et appelle le DeregisterImage et DeleteSnapshot API sur supprimer).

Étant donné que la création des AMI peut parfois prendre beaucoup de temps, une ressource personnalisée basée sur Lambda devra se ré-invoquer elle-même si l'attente n'est pas terminée avant l'expiration de la fonction Lambda.

Voici un exemple complet:

 Launch Stack

Description: Create an AMI from an EC2 instance.
Parameters:
  ImageId:
    Description: Image ID for base EC2 instance.
    Type: AWS::EC2::Image::Id
    # amzn-AMI-hvm-2016.09.1.20161221-x86_64-gp2
    Default: AMI-9be6f38c
  InstanceType:
    Description: Instance type to launch EC2 instances.
    Type: String
    Default: m3.medium
    AllowedValues: [ m3.medium, m3.large, m3.xlarge, m3.2xlarge ]
Resources:
  # Completes when the instance is fully provisioned and ready for AMI creation.
  AMICreate:
    Type: AWS::CloudFormation::WaitCondition
    CreationPolicy:
      ResourceSignal:
        Timeout: PT10M
  Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      UserData:
        "Fn::Base64": !Sub |
          #!/bin/bash -x
          yum -y install mysql # provisioning example
          /opt/aws/bin/cfn-signal \
            -e $? \
            --stack ${AWS::StackName} \
            --region ${AWS::Region} \
            --resource AMICreate
          shutdown -h now
  AMI:
    Type: Custom::AMI
    DependsOn: AMICreate
    Properties:
      ServiceToken: !GetAtt AMIFunction.Arn
      InstanceId: !Ref Instance
  AMIFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var response = require('cfn-response');
          var AWS = require('aws-sdk');
          exports.handler = function(event, context) {
            console.log("Request received:\n", JSON.stringify(event));
            var physicalId = event.PhysicalResourceId;
            function success(data) {
              return response.send(event, context, response.SUCCESS, data, physicalId);
            }
            function failed(e) {
              return response.send(event, context, response.FAILED, e, physicalId);
            }
            // Call ec2.waitFor, continuing if not finished before Lambda function timeout.
            function wait(waiter) {
              console.log("Waiting: ", JSON.stringify(waiter));
              event.waiter = waiter;
              event.PhysicalResourceId = physicalId;
              var request = ec2.waitFor(waiter.state, waiter.params);
              setTimeout(()=>{
                request.abort();
                console.log("Timeout reached, continuing function. Params:\n", JSON.stringify(event));
                var lambda = new AWS.Lambda();
                lambda.invoke({
                  FunctionName: context.invokedFunctionArn,
                  InvocationType: 'Event',
                  Payload: JSON.stringify(event)
                }).promise().then((data)=>context.done()).catch((err)=>context.fail(err));
              }, context.getRemainingTimeInMillis() - 5000);
              return request.promise().catch((err)=>
                (err.code == 'RequestAbortedError') ?
                  new Promise(()=>context.done()) :
                  Promise.reject(err)
              );
            }
            var ec2 = new AWS.EC2(),
                instanceId = event.ResourceProperties.InstanceId;
            if (event.waiter) {
              wait(event.waiter).then((data)=>success({})).catch((err)=>failed(err));
            } else if (event.RequestType == 'Create' || event.RequestType == 'Update') {
              if (!instanceId) { failed('InstanceID required'); }
              ec2.waitFor('instanceStopped', {InstanceIds: [instanceId]}).promise()
              .then((data)=>
                ec2.createImage({
                  InstanceId: instanceId,
                  Name: event.RequestId
                }).promise()
              ).then((data)=>
                wait({
                  state: 'imageAvailable',
                  params: {ImageIds: [physicalId = data.ImageId]}
                })
              ).then((data)=>success({})).catch((err)=>failed(err));
            } else if (event.RequestType == 'Delete') {
              if (physicalId.indexOf('AMI-') !== 0) { return success({});}
              ec2.describeImages({ImageIds: [physicalId]}).promise()
              .then((data)=>
                (data.Images.length == 0) ? success({}) :
                ec2.deregisterImage({ImageId: physicalId}).promise()
              ).then((data)=>
                ec2.describeSnapshots({Filters: [{
                  Name: 'description',
                  Values: ["*" + physicalId + "*"]
                }]}).promise()
              ).then((data)=>
                (data.Snapshots.length === 0) ? success({}) :
                ec2.deleteSnapshot({SnapshotId: data.Snapshots[0].SnapshotId}).promise()
              ).then((data)=>success({})).catch((err)=>failed(err));
            }
          };
      Runtime: nodejs4.3
      Timeout: 300
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal: {Service: [lambda.amazonaws.com]}
          Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
      Policies:
      - PolicyName: EC2Policy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
              - 'ec2:DescribeInstances'
              - 'ec2:DescribeImages'
              - 'ec2:CreateImage'
              - 'ec2:DeregisterImage'
              - 'ec2:DescribeSnapshots'
              - 'ec2:DeleteSnapshot'
              Resource: ['*']
Outputs:
  AMI:
    Value: !Ref AMI
30
wjordan

Pour ce que cela vaut, voici la variante Python de la définition AMIFunction de wjordan dans la réponse originale . Toutes les autres ressources du yaml d'origine restent inchangées:

AMIFunction:
  Type: AWS::Lambda::Function
  Properties:
    Handler: index.handler
    Role: !GetAtt LambdaExecutionRole.Arn
    Code:
      ZipFile: !Sub |
        import logging
        import cfnresponse
        import json
        import boto3
        from threading import Timer
        from botocore.exceptions import WaiterError

        logger = logging.getLogger()
        logger.setLevel(logging.INFO)

        def handler(event, context):

          ec2 = boto3.resource('ec2')
          physicalId = event['PhysicalResourceId'] if 'PhysicalResourceId' in event else None

          def success(data={}):
            cfnresponse.send(event, context, cfnresponse.SUCCESS, data, physicalId)

          def failed(e):
            cfnresponse.send(event, context, cfnresponse.FAILED, str(e), physicalId)

          logger.info('Request received: %s\n' % json.dumps(event))

          try:
            instanceId = event['ResourceProperties']['InstanceId']
            if (not instanceId):
              raise 'InstanceID required'

            if not 'RequestType' in event:
              success({'Data': 'Unhandled request type'})
              return

            if event['RequestType'] == 'Delete':
              if (not physicalId.startswith('AMI-')):
                raise 'Unknown PhysicalId: %s' % physicalId

              ec2client = boto3.client('ec2')
              images = ec2client.describe_images(ImageIds=[physicalId])
              for image in images['Images']:
                ec2.Image(image['ImageId']).deregister()
                snapshots = ([bdm['Ebs']['SnapshotId'] 
                              for bdm in image['BlockDeviceMappings'] 
                              if 'Ebs' in bdm and 'SnapshotId' in bdm['Ebs']])
                for snapshot in snapshots:
                  ec2.Snapshot(snapshot).delete()

              success({'Data': 'OK'})
            Elif event['RequestType'] in set(['Create', 'Update']):
              if not physicalId:  # AMI creation has not been requested yet
                instance = ec2.Instance(instanceId)
                instance.wait_until_stopped()

                image = instance.create_image(Name="Automatic from CloudFormation stack ${AWS::StackName}")

                physicalId = image.image_id
              else:
                logger.info('Continuing in awaiting image available: %s\n' % physicalId)

              ec2client = boto3.client('ec2')
              waiter = ec2client.get_waiter('image_available')

              try:
                waiter.wait(ImageIds=[physicalId], WaiterConfig={'Delay': 30, 'MaxAttempts': 6})
              except WaiterError as e:
                # Request the same event but set PhysicalResourceId so that the AMI is not created again
                event['PhysicalResourceId'] = physicalId
                logger.info('Timeout reached, continuing function: %s\n' % json.dumps(event))
                lambda_client = boto3.client('lambda')
                lambda_client.invoke(FunctionName=context.invoked_function_arn, 
                                      InvocationType='Event',
                                      Payload=json.dumps(event))
                return

              success({'Data': 'OK'})
            else:
              success({'Data': 'OK'})
          except Exception as e:
            failed(e)
    Runtime: python2.7
    Timeout: 300
5
Hynek Mlnarik
  1. Non.
  2. Je suppose que oui. Une fois la pile complétée, vous pouvez utiliser l'opération "Mettre à jour la pile". Vous devez fournir le modèle JSON complet de la pile initiale + vos modifications dans le même fichier (AMI modifié). Je l'exécuterais d'abord dans un environnement de test (pas de production), car je ne suis pas vraiment sûr du résultat de l'opération. instances existantes.

Pourquoi ne pas créer une AMI initialement en dehors de cloudformation, puis l’utiliser dans votre modèle de cloudformation final?

Une autre option consiste à écrire une partie de l’automatisation afin de créer deux piles d’informations cloud et vous pouvez supprimer la première une fois que l’AMI que vous avez créée est finalisée.

2
Rico

Bien que la solution de @ wjdordan soit adaptée aux cas d'utilisation simples, la mise à jour des données utilisateur ne mettra pas à jour l'AMI.

(AVERTISSEMENT: je suis l'auteur d'origine) cloudformation-AMI vise à vous permettre de déclarer des AMI dans CloudFormation pouvant être créées, mises à jour et supprimées de manière fiable. Utilisation de cloudformation-AMI Vous pouvez déclarer des AMI personnalisées comme ceci:

MyAMI:
  Type: Custom::AMI
  Properties:
    ServiceToken: !ImportValue AMILambdaFunctionArn
    Image:
      Name: my-image
      Description: some description for the image
    TemplateInstance:
      ImageId: AMI-467ca739
      IamInstanceProfile:
        Arn: arn:aws:iam::1234567890:instance-profile/MyProfile-ASDNSDLKJ
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -x
          yum -y install mysql # provisioning example
          # Signal that the instance is ready
          INSTANCE_ID=`wget -q -O - http://169.254.169.254/latest/meta-data/instance-id`
          aws ec2 create-tags --resources $INSTANCE_ID --tags Key=UserDataFinished,Value=true --region ${AWS::Region}
      KeyName: my-key
      InstanceType: t2.nano
      SecurityGroupIds:
      - sg-d7bf78b0
      SubnetId: subnet-ba03aa91
      BlockDeviceMappings:
      - DeviceName: "/dev/xvda"
        Ebs:
          VolumeSize: '10'
          VolumeType: gp2
1
spg