Skip to content

Latest commit

 

History

History
177 lines (154 loc) · 5.94 KB

CustomResources.md

File metadata and controls

177 lines (154 loc) · 5.94 KB

Idempotent S3 Bucket Creation with Custom Resources

Dave Liggat - [email protected] - contributed this example of how to properly structure Custom Resources. It has a self-contained custom resource for an S3 "idempotent bucket" that exports its ServiceToken ARN, and a "client" stack that makes use of it below.

AWSTemplateFormatVersion: "2010-09-09"

Description: A 'idempotent_bucket_creator' custom resource.

Parameters:
  ResourcePrefix:
    Type: String
    Description: A description to identify resources  (e.g. "my-perf-test")
    MinLength: 2

Resources:
  CustomResourceLambdaFunction:
    Type: AWS::Lambda::Function
    Metadata:
      Comment:
        "Fn::Sub":
          "Function for ${ResourcePrefix}"
    DependsOn: [ CustomResourceLambdaFunctionExecutionRole ]
    Properties:
      Code:
        ZipFile:
          "Fn::Sub": |
            import boto3
            import json
            import logging
            import traceback
            import cfnresponse

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

            def update_resource(event, context):
                return {'CreatedByCustomResource': 'unknown'}

            def delete_resource(event, context):
                return {'CreatedByCustomResource': 'unknown'}

            def create_resource(event, context):
                client = boto3.client('s3')
                my_buckets = [ b['Name'] for b in client.list_buckets()['Buckets'] ]
                bucket_name = event['ResourceProperties']['Name']
                if bucket_name in my_buckets:
                    created = 'false'
                else:
                    result = client.create_bucket(Bucket=bucket_name)
                    logger.info('Result: ' + json.dumps(result))
                    created = 'true'
                return {'CreatedByCustomResource': created}, bucket_name

            def handler(event, context):
                logger.info('Event: ' + json.dumps(event))
                logger.info('Context: ' + str(dir(context)))
                operation = event['RequestType']
                physical_id = None
                data = { }
                try:
                    if operation == 'Create':
                        data, physical_id = create_resource(event, context)
                    elif operation == 'Delete':
                        data = delete_resource(event, context)
                    else:
                        data = update_resource(event, context)
                except Exception as e:
                    logger.error('CloudFormation custom resource {0} failed. Exception: {1}'.format(operation, traceback.format_exc()))
                    status = cfnresponse.FAILED
                else:
                    status = cfnresponse.SUCCESS
                    logger.info('CloudFormation custom resource {0} succeeded. Result data {1}'.format(operation, json.dumps(data)))
                cfnresponse.send(event, context, status, data, physical_id)


      Role: { "Fn::GetAtt": [ CustomResourceLambdaFunctionExecutionRole, Arn ] }
      Timeout: "30"  # Seconds.
      Handler: index.handler
      Runtime: python2.7
      MemorySize: "128"  # MB.

  Policy:
    Type: AWS::IAM::Policy
    Properties:
      Roles:
        - { Ref: CustomResourceLambdaFunctionExecutionRole }
      PolicyName: CommonPolicyForLambdaAndDevelopment
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource: "arn:aws:logs:*:*:*"
          - Effect: Allow
            Action:
              - "s3:CreateBucket"
              - "s3:ListAllMyBuckets"
            Resource: "*"


  CustomResourceLambdaFunctionExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: [ lambda.amazonaws.com ]
            Action:
              - sts:AssumeRole
          - Effect: Allow
            Principal:
              AWS:
                - "Fn::Join":
                  - ""
                  - - "arn:aws:iam::"
                    - { Ref: "AWS::AccountId" }
                    - ":"
                    - "root"
            Action:
              - sts:AssumeRole
      Path: /

Outputs:
  CustomResourceLambdaFunction:
    Value: { Ref : CustomResourceLambdaFunction }
  CustomResourceLambdaFunctionARN:
    Value: { "Fn::GetAtt": [ CustomResourceLambdaFunction, Arn ] }
    Export:
      Name: CustomResourceArn-IdempotentBucketCreator
  CustomResourceLambdaFunctionExecutionRole:
    Value: { Ref : CustomResourceLambdaFunctionExecutionRole }
  CustomResourceLambdaFunctionExecutionRoleARN:
    Value: { "Fn::GetAtt": [ CustomResourceLambdaFunctionExecutionRole, Arn ] }
  SigninUrl:
    Value:
      "Fn::Sub": |
        https://signin.aws.amazon.com/switchrole?account=${AWS::AccountId}&roleName=${CustomResourceLambdaFunctionExecutionRole}&displayName=assumed-role
  TestCommand:
    Value:
      "Fn::Sub": |
        aws lambda invoke --function-name ${CustomResourceLambdaFunction} /tmp/${CustomResourceLambdaFunction}-output.txt; cat /tmp/${CustomResourceLambdaFunction}-output.txt

Custom Resource "client" stack that leverages the above custom resource.

AWSTemplateFormatVersion: "2010-09-09"

Description: An idempotent bucket creator client.

Parameters:
  BucketName:
    Type: String

Resources:
  IdempotentBucket1:
    Type: Custom::IdempotentBucket
    Properties:
      ServiceToken: { "Fn::ImportValue": CustomResourceArn-IdempotentBucketCreator }
      Region: { Ref: "AWS::Region" }
      Name: { Ref: BucketName }

Outputs:
  IdempotentBucket1:
    Value: { Ref: IdempotentBucket1 }
  IdempotentBucketCreatedByCustomResource1:
    Value:
      "Fn::GetAtt": [IdempotentBucket1, CreatedByCustomResource]