Blog

Die Erstellung von benutzerdefinierten CloudFormation-Ressourcen ist schlicht und einfach

Martijn van Dongen

Aktualisiert Oktober 21, 2025
5 Minuten

Ich habe festgestellt, dass die meisten Blogbeiträge und Dokumentationen über Custom Resources für CloudFormation sehr kompliziert sind. Sie sind perfekt für erfahrene Benutzer, aber es ist ziemlich schwierig, sie zum ersten Mal zu verwenden. Dieser Blog-Beitrag ist wirklich einfach als Ihr erstes CloudFormation Custom Resource-Projekt zu verwenden und eignet sich im Allgemeinen gut für die meisten Anwendungsfälle. Spoiler: Der gesamte Custom Resource Stack ist eine einzige Datei. Er muss nicht gepackt und auf S3 hochgeladen werden und wird mit einem einzigen Befehl bereitgestellt.

Intro

Betrachten Sie die folgende CloudFormation-Vorlage. BucketName ist keine erforderliche Eigenschaft, aber nehmen wir an, dass sie es ist und Sie möchten, dass sie zufällig generiert wird. Einige andere Ressourcen zwingen Sie dazu, einen festen Namen einzugeben. Sie können dieselbe Vorlage nicht noch einmal bereitstellen, oder Sie müssen einen zufälligen Namen als Parameter angeben. Ich möchte, dass alle meine Ressourcen eine zufällige Endung erhalten, also brauche ich eine Art RandomString-Funktion in meiner CloudFormation-Vorlage.

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: "myfixedbucketname"

Bereitstellen einer benutzerdefinierten Ressource Lambda

Es ist nun an der Zeit, die Lambda-Funktion für die benutzerdefinierte Ressource bereitzustellen. In vielen Fällen besteht eine Lambda-Funktion aus mehreren Dateien und Build- und Bereitstellungsschritten. In diesem Beispiel handelt es sich nur um eine einzige CloudFormation-Datei mit Inline-Python-Code.

Resources:
  RandomString:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import base64
          import json
          import logging
          import string
          import random
          import boto3
          from botocore.vendored import requests
          import cfnresponse

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

          def random_string(size=6):
            return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(size))

          def lambda_handler(event, context):
            logger.info('got event {}'.format(event))
            responseData = {}

            if event['RequestType'] == 'Create':
              number = int(event['ResourceProperties'].get('Number', 6))
              rs = random_string(number)
              responseData['upper'] = rs.upper()
              responseData['lower'] = rs.lower()

            else: # delete / update
              rs = event['PhysicalResourceId'] 
              responseData['upper'] = rs.upper()
              responseData['lower'] = rs.lower()

            logger.info('responseData {}'.format(responseData))
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, responseData['lower'])

      FunctionName: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:random-string'
      Handler: "index.lambda_handler"
      Timeout: 30
      Role: !GetAtt 'LambdaRole.Arn'
      Runtime: python3.6
  # The LambdaRole is very simple for this use case, because it only need to have access to write logs
  # If the lambda is going to access AWS services using boto3, this role must be
  # extended to give lambda the appropriate permissions.
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: "lambda-logs"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - "arn:aws:logs:*:*:*"

Verteilen Sie diese Vorlage nun mit dem folgenden Befehl.

aws cloudformation create-stack 
    --stack-name cfn-custom-resources 
    --template-body file://template.yaml 
    --capabilities CAPABILITY_IAM

Verwenden Sie die benutzerdefinierte Ressource

Jetzt, wo die Lambda-Funktion Custom Resource zu funktionieren scheint, ist es an der Zeit, unsere CloudFormation-Vorlage so zu ändern, dass sie die Custom Resource verwendet, um einen zufälligen String für unseren S3-Bucket zu generieren.

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "mybucket-${RandomString.lower}"
  # returns attributes: lower and upper
  RandomString:
    Type: Custom::RandomString
    Properties:
      ServiceToken:
        !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:random-string"
      Number: 8

Durch die Angabe von Number: 8 können Sie die Vorgabe von 6 Ziffern in eine längere zufällige Zeichenfolge ändern. Wir verwenden in diesem Beispiel die Zeichenkette "lower", da ein S3-Bucket klein geschrieben werden muss.
Probieren Sie es aus und sehen Sie, was passiert. Sie sollten nun einen Bucket haben, dessen Bucketname mit "mybucket-" beginnt, gefolgt von einer zufälligen Zeichenfolge. Sie können eine Aktualisierung der Ressource RandomString erzwingen, indem Sie Ihrem CloudFormation Stack Tags hinzufügen. Die zufällige Zeichenkette wird nicht neu generiert, da die PhysicalResourceId die Zufallszahl ist, können wir diese einfach als Eingabe verwenden.

Nächste Schritte & Schlussfolgerung

Einige Schlussfolgerungen:

  • Es ist einfach, eine benutzerdefinierte CloudFormation-Ressource in einer einzigen CloudFormation-Vorlage zu erstellen
  • Es ist einfach, die Lambda-Funktion zu schreiben, bereitzustellen und zu testen, einschließlich der Rolle und der IAM-Richtlinie für den Zugriff auf die AWS-Ressourcen
  • Das Aktualisieren des Stacks und das Ausführen des Tests können Sie ganz einfach mit einem einzigen Bash-Befehl erledigen, oder indem Sie die Bereitstellung in das Testskript einfügen
  • Mit dieser benutzerdefinierten RandomString-Ressource ist es einfach, mehrere Vorlagen gleichzeitig zu testen, da jede Ressource einen eindeutigen Namen hat.

Einzelne Vorlage

Jemand hat mich gefragt, ob es auch möglich ist, eine benutzerdefinierte Ressource zusammen mit dem Rest Ihres Stacks hinzuzufügen. Die Antwort ist ja, und dies ist die Beispielvorlage:

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  InputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'yourbucket-${RandomName}'

  RandomName:
    Type: Custom::RandomNameGenerator
    Properties:
      ServiceToken: !GetAtt 'RandomNameGenerator.Arn'

  RandomNameGenerator:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      Timeout: 30
      Role: !GetAtt 'LambdaBasicExecutionRole.Arn'
      Runtime: python3.6
      Code:
        ZipFile: |
          import base64
          import json
          import logging
          import string
          import random
          import boto3
          from botocore.vendored import requests
          import cfnresponse

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

          def random_string(size=6):
            return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(size))

          def lambda_handler(event, context):
            logger.info('got event {}'.format(event))
            responseData = {}

            if event['RequestType'] == 'Create':
              number = int(event['ResourceProperties'].get('Number', 6))
              rs = random_string(number)
              responseData['upper'] = rs.upper()
              responseData['lower'] = rs.lower()

            else: # delete / update
              rs = event['PhysicalResourceId'] 
              responseData['upper'] = rs.upper()
              responseData['lower'] = rs.lower()

            logger.info('responseData {}'.format(responseData))
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, responseData['lower'])

  LambdaBasicExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
          Condition: {}
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Verfasst von

Martijn van Dongen

Contact

Let’s discuss how we can support your journey.