BLOG

AWS Lambda event notification을 사용하여 Amazon S3 bucket 프로비저닝의 순환 종속성 (Circular Dependency) 해결하기
작성일: 2018-10-22

 

개요

AWS CloudFormation은 클라우드 환경의 모든 인프라 자원을 설명하고 프로비저닝 할 수 있는 공용 언어를 제공합니다. CloudFormation을 사용하면 간단한 텍스트 파일을 사용하여 모든 AWS 리전 및 계정에서 응용 프로그램에 필요한 모든 리소스를 자동화되고 안전한 방법으로 모델링하고 프로비저닝 할 수 있습니다.

 

CloudFormation 템플릿을 여러 리전 및 환경에서 쉽게 테스트 및 배포 할 수 있기 때문에 리소스 이름을 자동으로 생성하는 것이 AWS CloudFormation 의 베스트 프랙티스가 될 수 있습니다. 예를 들어, Amazon S3 버킷은 전역적으로 고유한 이름을 가져야 합니다. CloudFormation 템플릿에서 버킷 이름을 하드 코딩하면 코드가 망가지기 쉽고 배포 또는 테스트하기가 쉽지 않습니다. 이런 경우에는, 전 세계적으로 다른 고객이 같은 이름의 S3 버킷을 만들기 위해 우연히 선택했을 가능성이 있습니다. 따라서 가능하면 명시적으로 리소스의 이름을 지정하지 않는 것이 좋습니다. 대신 CloudFormation이 충돌 및 비결정 오류를 피하기 위해 이름을 생성하도록 하세요.

 

동시에 AWS Lambda 함수가 지원되는 이벤트 알림을 사용하여 Amazon S3 버킷을 만드는 것이 일반적입니다. 고객이 CloudFormation에이 설정을 배포하려고 시도하면 Lambda 함수와 S3 버킷 사이의 순환 종속성으로 (circular dependency) 인해 배포에 실패할 수 있습니다. 이는 Lambda 함수를 생성하기 전에 Lambda 권한이 버킷의 고유한 Amazon Resource Name (ARN)을 알아야 하기때문에 버킷은 Lambda 함수의 고유 ARN을 알아야만 버킷을 생성 할 수 있기 때문입니다.

 

이 기사에서는 CloudFormation 사용자 지정 리소스를 사용하여 자동 생성된 S3 버킷 이름의 원하는 결과를 유지하면서 순환 종속성을 해결하는 메커니즘을 제시합니다. 이 리소스를 사용하면 템플릿과 관련된 Lambda 함수에 사용자 지정 프로비저닝 로직을 쓸 수 있습니다.

 

순환 종속성 데모

단순히 이벤트를 기록하는 Lambda 함수를 트리거하는 이벤트로 S3 버킷을 설정하는 다음 템플릿을 고려하십시오.

 

[YAML]

AWSTemplateFormatVersion: ‘2010-09-09’

Description: Sample template that demonstrates the circular dependency

Resources:

  TestFunctionRole:

    Type: AWS::IAM::Role

    Properties:

      AssumeRolePolicyDocument:

        Version: ‘2012-10-17’

        Statement:

          – Effect: Allow

            Principal:

              Service: lambda.amazonaws.com

            Action: sts:AssumeRole

      ManagedPolicyArns:

        – arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

      Path: /

 

  TestEventFunction:

    Type: AWS::Lambda::Function

    Properties:

      Description: Dummy function, simply logs received S3 events

      Handler: index.handler

      Runtime: python2.7

      Role: !GetAtt ‘TestFunctionRole.Arn’

      Timeout: 240

      Code:

        ZipFile: |

          import json

          import logging

          logger = logging.getLogger()

          logger.setLevel(logging.DEBUG)

 

          def handler(event, context):

              logger.info(‘Received event: %s’ % json.dumps(event))

 

  TestS3BucketEventPermission:

    Type: AWS::Lambda::Permission

    Properties:

      Action: lambda:invokeFunction

      SourceAccount: !Ref ‘AWS::AccountId’

      FunctionName: !Ref ‘TestEventFunction’

      SourceArn: !GetAtt

        – TestS3Bucket

        – Arn

      Principal: s3.amazonaws.com

 

  TestS3Bucket:

    Type: AWS::S3::Bucket

    DependsOn: TestS3BucketEventPermission

    Properties:

      NotificationConfiguration:

        LambdaConfigurations:

          –

            Function:

              “Fn::GetAtt”:

                – ‘TestEventFunction’

                – ‘Arn’

            Event: s3:ObjectCreated:*

 

이 템플릿을 시작하려고 하면 다음과 같은 오류가 발생합니다.

Circular dependency between resources: [TestS3BucketEventPermission, TestS3Bucket].

 

TestS3BucketEventPermission은 TestEventFunction을 만들기 전에 TestS3Bucket의 ARN을 알아야 하기 때문입니다. 그러나 TestS3Bucket의 NotificationConfiguration은 TestS3Bucket을 만들기 전에 TestEventFunction의 ARN을 알아야 하므로 순환 종속성이 생성됩니다.

 

대신 TestS3Bucket의 DependsOn 속성을 TestEventFunction을 가리키도록 변경하면 “다음 대상 구성의 유효성을 검사할 수 없습니다.” 라는 다른 오류가 발생합니다.

 

사용자 지정 리소스를 사용하는 솔루션

알림 구성없이 S3 버킷을 생성함으로써 앞에서 설명한 순환 종속성을 피할 수 있습니다. 그런 다음 Lambda 함수, Lambda 권한 및 S3 버킷 자원이 작성된 후 알림을 설정합니다. 이는 CLI를 사용하여 수동으로 수행 할 수 있지만 CloudFormation 사용자 정의 리소스는 CloudFormation 자체에서 자동화된 배포 라이프 사이클의 일부로서 로직을 포함 할 수 있는 좋은 메커니즘을 제공합니다.

 

다음 예제에서는 S3 버킷과 Lambda 함수 ARN을 이벤트 입력으로 사용하고 제공된 Lambda 함수로 NotificationConfiguration을 S3 버킷에 적용하는 새로운 Lambda 함수인 ApplyBucketNotificationFunction을 만듭니다.

 

참고 : 데모용으로 코드 인라인을 포함 시켰습니다. S3에서 Lambda 코드 zip 파일을 가져 와서 코드 유지 관리를 향상시킬 수 있습니다.

 

[YAML]

AWSTemplateFormatVersion: ‘2010-09-09’

Description: Sample template that demonstrates setting up Lambda event notification

  for an S3 bucket using a custom resource

 

Resources:

  TestFunctionRole:

    Type: AWS::IAM::Role

    Properties:

      AssumeRolePolicyDocument:

        Version: ‘2012-10-17’

        Statement:

          – Effect: Allow

            Principal:

              Service: lambda.amazonaws.com

            Action: sts:AssumeRole

      ManagedPolicyArns:

        – arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

      Path: /

 

  TestEventFunction:

    Type: AWS::Lambda::Function

    Properties:

      Description: Dummy function, simply logs received S3 events

      Handler: index.handler

      Runtime: python2.7

      Role: !GetAtt ‘TestFunctionRole.Arn’

      Timeout: 240

      Code:

        ZipFile: |

          import json

          import logging

          logger = logging.getLogger()

          logger.setLevel(logging.DEBUG)

 

          def handler(event, context):

              logger.info(‘Received event: %s’ % json.dumps(event))

 

  TestS3BucketEventPermission:

    Type: AWS::Lambda::Permission

    Properties:

      Action: lambda:invokeFunction

      SourceAccount: !Ref ‘AWS::AccountId’

      FunctionName: !Ref ‘TestEventFunction’

      SourceArn: !GetAtt

        – TestS3Bucket

        – Arn

      Principal: s3.amazonaws.com

 

  TestS3Bucket:

    Type: AWS::S3::Bucket

    DependsOn: TestEventFunction

 

  ApplyNotificationFunctionRole:

    Type: AWS::IAM::Role

    Properties:

      AssumeRolePolicyDocument:

        Version: ‘2012-10-17’

        Statement:

          – Effect: Allow

            Principal:

              Service: lambda.amazonaws.com

            Action: sts:AssumeRole

      ManagedPolicyArns:

        – arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

      Path: /

      Policies:

        – PolicyName: S3BucketNotificationPolicy

          PolicyDocument:

            Version: ‘2012-10-17’

            Statement:

              – Sid: AllowBucketNotification

                Effect: Allow

                Action: s3:PutBucketNotification

                Resource:

                  – !Sub ‘arn:aws:s3:::${TestS3Bucket}’

                  – !Sub ‘arn:aws:s3:::${TestS3Bucket}/*’

 

  ApplyBucketNotificationFunction:

    Type: AWS::Lambda::Function

    Properties:

      Description: Dummy function, just logs the received event

      Handler: index.handler

      Runtime: python2.7

      Role: !GetAtt ‘ApplyNotificationFunctionRole.Arn’

      Timeout: 240

      Code:

        ZipFile: |

          import boto3

          import logging

          import json

          import cfnresponse

 

          s3Client = boto3.client(‘s3’)

          logger = logging.getLogger()

          logger.setLevel(logging.DEBUG)

 

          def addBucketNotification(bucketName, notificationId, functionArn):

            notificationResponse = s3Client.put_bucket_notification_configuration(

              Bucket=bucketName,

              NotificationConfiguration={

                ‘LambdaFunctionConfigurations’: [

                  {

                    ‘Id’: notificationId,

                    ‘LambdaFunctionArn’: functionArn,

                    ‘Events’: [

                      ‘s3:ObjectCreated:*’

                    ]

                  },

                ]

              }

            )

            return notificationResponse

 

          def create(properties, physical_id):

            bucketName = properties[‘S3Bucket’]

            notificationId = properties[‘NotificationId’]

            functionArn = properties[‘FunctionARN’]

            response = addBucketNotification(bucketName, notificationId, functionArn)

            logger.info(‘AddBucketNotification response: %s’ % json.dumps(response))

            return cfnresponse.SUCCESS, physical_id

 

          def update(properties, physical_id):

            return cfnresponse.SUCCESS, None

 

          def delete(properties, physical_id):

            return cfnresponse.SUCCESS, None

 

          def handler(event, context):

            logger.info(‘Received event: %s’ % json.dumps(event))

 

            status = cfnresponse.FAILED

            new_physical_id = None

 

            try:

              properties = event.get(‘ResourceProperties’)

              physical_id = event.get(‘PhysicalResourceId’)

 

              status, new_physical_id = {

                ‘Create’: create,

                ‘Update’: update,

                ‘Delete’: delete

              }.get(event[‘RequestType’], lambda x, y: (cfnresponse.FAILED, None))(properties, physical_id)

            except Exception as e:

              logger.error(‘Exception: %s’ % e)

              status = cfnresponse.FAILED

            finally:

              cfnresponse.send(event, context, status, {}, new_physical_id)

 

  ApplyNotification:

    Type: Custom::ApplyNotification

    Properties:

      ServiceToken: !GetAtt ‘ApplyBucketNotificationFunction.Arn’

      S3Bucket: !Ref ‘TestS3Bucket’

      FunctionARN: !GetAtt ‘TestEventFunction.Arn’

      NotificationId: S3ObjectCreatedEvent

 

결론

이 글의 시작 부분에서 저희는 Lambda 권한 (TestS3BucketEventPermission)과 S3 버킷 (TestS3Bucket) 리소스의 NotificationConfiguration이 서로 종속되어 있어 CloudFormation이 리소스를 만들 수 없음을 알 수 있었습니다.

 

또한 저는 이 글에서 순환 종속성을 피하는 솔루션 예제를 제공했습니다. 먼저 알림 구성없이 TestS3Bucket을 만들었습니다. 그 후, TestS3Bucket과 TestEventFunction이 모두 생성 된 후 S3 버킷에 Lambda 알림 구성을 추가하는 ApplyBucketNotificationFunction을 호출하는 사용자 지정 리소스 ApplyNotification을 사용했습니다.

 

이 솔루션에는 두 가지 장점이 있습니다.

 

첫째, S3 버킷 이름은 전 지역에서 고유해야 하므로 하드 코드 버킷 이름이 필요한 모든 솔루션은 이름 충돌시 불확실한 배포 오류가 발생할 위험이 있습니다. 이 접근법은 S3 버킷에 고정된 이름을 부여 할 필요가 없으므로 해당 시나리오를 방지합니다.

 

둘째, 사용자 정의 리소스를 사용하여 버킷 알림을 CloudFormation 배포의 일부로 적용했습니다. 이렇게 하면 스택을 성공적으로 배포 한 후 버킷 알림을 설정하는 데 필요한 수동 또는 스크립팅 작업이 필요하지 않습니다.

 

레퍼런스

다음 두 개의 템플릿에 대한 코드 액세스를 드립니다.

 

 

원문 URL: https://aws.amazon.com/ko/blogs/mt/resolving-circular-dependency-in-provisioning-of-amazon-s3-buckets-with-aws-lambda-event-notifications/

** 메가존클라우드 TechBlog는 AWS BLOG 영문 게재글중에서 한국 사용자들에게 유용한 정보 및 콘텐츠를 우선적으로 번역하여 내부 엔지니어 검수를 받아서, 정기적으로 게재하고 있습니다. 추가로 번역및 게재를 희망하는 글에 대해서 관리자에게 메일 또는 SNS페이지에 댓글을 남겨주시면, 우선적으로 번역해서 전달해드리도록 하겠습니다.