Pytanie Utwórz obraz AMI jako część zestawu chmurowego


Chcę utworzyć stos chmurowy EC2, który zasadniczo można opisać w następujących krokach:

1.- Uruchomienie instancji

2.- Zapewnienie instancji

3. Zatrzymaj instancję i utwórz z niej obraz AMI

4.- Utwórz grupę autoskalowania z utworzonym obrazem AMI jako źródłem do uruchomienia nowych instancji.

Zasadniczo mogę zrobić 1 i 2 w jednym szablonie chmurki, a 4 w drugim szablonie. Nie wydaje mi się, aby utworzyć obraz AMI z instancji wewnątrz szablonu Cloudformation, który zasadniczo generuje problem ręcznego usuwania AMI, jeśli chcę usunąć stos.

Biorąc to pod uwagę, moje pytania to:

1.- Czy istnieje sposób na utworzenie obrazu AMI z instancji WEWNĄTRZ szablonu chmury?

2.- Jeśli odpowiedź na pytanie 1 brzmi "nie", czy istnieje sposób na dodanie obrazu AMI (lub jakiegokolwiek innego zasobu), aby był on częścią ukończonego stosu?

EDYTOWAĆ:

Aby wyjaśnić, już rozwiązałem problem tworzenia AMI i używania go w szablonie chmurowym, po prostu nie mogę utworzyć AMI WEWNĄTRZ szablonu chmury lub dodać go w jakiś sposób do utworzonego stosu.

Jak powiedziałem na temat odpowiedzi Rico, to co teraz robię, to używanie playbacku ansibli, który zasadniczo ma 3 kroki:

1.- Utwórz instancję podstawową za pomocą szablonu Cloudformation

2.- Utwórz, używając ansible, AMI instancji utworzonej w kroku 1

3.- Utwórz pozostałą część stosu (ELB, grupy automatycznego skalowania itp.) Za pomocą drugiego szablonu chmurki, który aktualizuje szablon utworzony w kroku 1, i który wykorzystuje AMI utworzony w kroku 2 w celu uruchomienia instancji.

Tak to teraz zarządzam, ale chciałem się dowiedzieć, czy istnieje jakiś sposób na stworzenie AMI INSIDE w szablonie cloudformation lub jeśli możliwe jest dodanie utworzonego AMI do stosu (coś w rodzaju mówienia stosowi, "Hej, to należy do ty również, więc zrób to ").


14
2018-01-29 12:40


pochodzenie




Odpowiedzi:


Tak, możesz utworzyć AMI z instancji EC2 w szablonie CloudFormation przez implementację Niestandardowy zasób który wywołuje Stwórz obraz API przy tworzeniu (i wywołuje DeregisterImage i DeleteSnapshot API przy usuwaniu).

Ponieważ tworzenie AMI może czasami zająć dużo czasu, zasoby niestandardowe wspierane przez Lambda będą musiały zostać ponownie wywołane, jeśli oczekiwanie nie zakończy się, zanim upłynie limit czasu Lambdy.

Oto pełny przykład:

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

19
2018-01-16 17:25





  1. Nie.
  2. Przypuszczam, że tak. Po stosie możesz użyć operacji "Aktualizuj stos". Musisz podać pełny szablon JSON początkowego stosu + twoje zmiany w tym samym pliku (zmiana AMI) Najpierw uruchomiłbym to w środowisku testowym (nie produkcyjnym), ponieważ nie jestem do końca pewien, co robi operacja istniejące instancje.

Dlaczego nie utworzyć AMI początkowo poza formą chmury, a następnie użyć tego AMI w ostatecznym szablonie chmurowym?

Inną opcją jest napisanie automatyzacji w celu utworzenia dwóch stosów chmurek i możesz usunąć pierwszy, gdy skończony AMI zostanie sfinalizowany.


2
2018-01-29 17:01



Rico, jeśli się nie mylę (i tak jak teraz to robię, nie sądzę), możesz zmodyfikować stos po stworzeniu, aktualizując go. Pomysł stworzenia zewnętrznej formacji chmurowej AMI jest taki, jak sobie z tym radzę. Zasadniczo używam ansiblioteki z trzema krokami: 1. Tworzenie instancji za pomocą chmurki 2. Tworzenie AMI tej instancji za pomocą ansibla 3. -Utwórz resztę stosu (aktualizując utworzony) używając AMI utworzonego z ansibla Moje pytanie faktycznie wskazuje na wykonanie części AMI stosu lub jako część kroków cloudformation. Zaktualizuję moje pytanie, aby wyjaśnić. - dibits
@Dibits Got ya. Więc zmieniłem swoją odpowiedź na numer 2. Teraz, gdy pamiętam, jest operacja "Aktualizuj stos". Musisz podać pełny szablon JSON początkowego stosu + twoje zmiany w tym samym pliku. - Rico
Jeśli rozumiem, co masz na myśli, to po prostu zaktualizowałbym identyfikator AMI w szablonie cloudformation, ale nie scaliłbym obrazu AMI w stos. Być może, aby wyjaśnić jeszcze bardziej, ponieważ robię wszystko w jednym stosie, chcę móc usunąć ten stos i mieć AMI tworzyć wyrejestrowane automagicznie ze stosu (jak to się dzieje z instancjami, elbami i tak dalej). - dibits
Tak mi się wydaje. Nie próbowałem tego sam. Byłby ciekawy, jaki jest wynik. - Rico


Jeśli chodzi o to, co jest warte, oto wariant w języku Wythan w wersji Python AMIFunction definicja w oryginalnej odpowiedzi. Wszystkie inne zasoby w oryginalnym pliku yaml pozostają niezmienione:

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

2
2017-12-14 20:26





Podczas gdy rozwiązanie @ wjdordan jest dobre dla prostych przypadków użycia, aktualizacja danych użytkownika nie spowoduje aktualizacji interfejsu AMI.

(ZASTRZEŻENIE: jestem oryginalnym autorem) cloudformation-ami ma na celu umożliwienie deklarowania AMI w CloudFormation, które można wiarygodnie tworzyć, aktualizować i usuwać. Za pomocą cloudformation-ami Możesz zadeklarować niestandardowe AMI w następujący sposób:

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

0
2018-06-14 16:51



O ile to jest w ogóle możliwe, stosunkowo łatwo jest poszerzyć moją odpowiedź, aby zaktualizować AMI po aktualizacji UserData, po prostu tworząc nowe WaitCondition i Custom::AMI zasoby (lub zmiana nazwy Logical ID istniejących definicji, które utworzą nowy i zniszczą stare). W przypadku mojego nie-prostego przypadku użycia zawijam szablon CloudFormation za pomocą szablonu ERB i dołączam część wartości skrótu zatwierdzenia w identyfikatorze logicznym, np. AMICreate<%=commit%> i AMI<%=commit%>. - wjordan
Tak, to ma sens! Dzięki za wskazówkę - spg