20

enter image description here

I am working on AWS CloudFormation and I created one template in which I asked user to select Environment.

On the basis of selected value I created the resources. User have to select between DEV, QA, PROD, UAT etc. but when I suffix this value to S3 bucket name (-downloads.com) it not allowed because capital letter is not allowed in S3 bucket name.

So I did change in JSON where I use fn::Transform with "Condition":"Lower" but then while creating resources below error occurs.

No transform named 871247504605::String found.. Rollback requested by user.

Below is my CloudFormation JSON

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Provides nesting for required stacks to deploy a full resource of ****",
    "Metadata": {
        "AWS::CloudFormation::Interface": {
            "ParameterGroups": [
                {
                    "Label": {
                        "default": "Enviroment Selection"
                    },
                    "Parameters": [
                        "selectedEnv"
                    ]
                }
            ],
            "ParameterLabels": {
                "selectedEnv": {
                    "default": "Please select Enviroment"
                }
            }
        }
    },
    "Parameters": {
        "selectedEnv": {
            "Type": "String",
            "Default": "DEV",
            "AllowedValues": [
                "DEV",
                "QA",
                "UAT",
                "PROD"
            ]
        }
    },
    "Resources": {
        "S3BucketName": {
            "Type": "AWS::S3::Bucket",
            "Properties": {
                "BucketName": {
                    "Fn::Join": [
                        "",
                        [
                            {
                                "Fn::Transform": {
                                    "Name": "MyString",
                                    "Parameters": {
                                        "InputString": {
                                            "Ref": "selectedEnv"
                                        },
                                        "Operation": "Lower"
                                    }
                                }
                            },
                            "-deployment.companyname.com"
                        ]
                    ]
                },
                "PublicAccessBlockConfiguration": {
                    "BlockPublicAcls": "true",
                    "BlockPublicPolicy": "true",
                    "IgnorePublicAcls": "true",
                    "RestrictPublicBuckets": "true"
                },
                "Tags": [
                    {
                        "Key": "ENV",
                        "Value": {
                            "Ref": "selectedEnv"
                        }
                    },
                    {
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "",
                                [
                                    {
                                        "Ref": "selectedEnv"
                                    },
                                    "deployments"
                                ]
                            ]
                        }
                    }
                ]
            },
            "Metadata": {
                "AWS::CloudFormation::Designer": {
                    "id": "c81705e6-6c88-4a3d-bc49-80d8736bd88e"
                }
            }
        },
        "QueueForIOT": {
            "Type": "AWS::SQS::Queue",
            "Properties": {
                "QueueName": {
                    "Fn::Join": [
                        "",
                        [
                            {
                                "Ref": "selectedEnv"
                            },
                            "QueueForIOT"
                        ]
                    ]
                },
                "DelaySeconds": "0",
                "MaximumMessageSize": "262144",
                "MessageRetentionPeriod": "345600",
                "ReceiveMessageWaitTimeSeconds": "20",
                "VisibilityTimeout": "30"
            },
            "Metadata": {
                "AWS::CloudFormation::Designer": {
                    "id": "6484fbb7-a188-4a57-a40e-ba9bd69d4597"
                }
            }
        }
    },
    "Outputs": {
        "Help": {
            "Description": "This is description",
            "Value": ""
        }
    }
}

My question is, I want to do lowercase or sometimes uppercase value for S3 bucket or any other resources. How to do this?

Image of template creation error attached.

halfer
  • 19,824
  • 17
  • 99
  • 186
Vikramsinh Gaikwad
  • 827
  • 1
  • 9
  • 23
  • 3
    The JSON is invalid, can you share the full cloudformatino template? – Amit Baranes Jan 06 '20 at 10:01
  • I have updated the question and JSON also. Please help me out. Thanks – Vikramsinh Gaikwad Jan 07 '20 at 06:11
  • 1
    It's not clear from your question if you have lambda function implementing `MyString` macro which is required by aws in order to create custom transformations. More info can be found [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-macros.html). Example (including the lambda function) can be found [here](https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/services/CloudFormation/MacrosExamples/StringFunctions/string.yaml). – Molecular Man Jan 07 '20 at 08:45
  • No I just want to run above json(Template) in AWS CloudFormation which ask user to select environment. – Vikramsinh Gaikwad Jan 07 '20 at 11:35
  • 2
    You could just lower case the user inputs i.e. DEV=dev – George Rushby Jan 07 '20 at 12:38
  • but there is no any way to do with JSON/YAML only? only one input I have to get from User. If I get small dev then in other resources I need DEV value then what should I do? – Vikramsinh Gaikwad Jan 07 '20 at 13:38
  • 2
    you can't do this transformation with json/yaml only. Though what you could do with json/yaml only is to map `DEV to dev, PROD to prod, etc` by incorporating [mappings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html) and [Fn::FindInMap](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html) into your template. – Molecular Man Jan 08 '20 at 10:23
  • @MolecularMan Hey thanks Man. I did it finally. Thank you so much. – Vikramsinh Gaikwad Jan 13 '20 at 09:58

5 Answers5

6

Mr. Young is correct, that is the syntax you need to use to invoke the macros.

HOWEVER, the key factor which both they and the documentation failed to mention is that in order to invoke the transform macros that you need to deploy this stack into your accounts BEFORE you can use the functions listed in the ReadMe.

https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/services/CloudFormation/MacrosExamples/StringFunctions/string.yaml

I think the docs could be clarified in this regard, I'll see if I can PR a clarification

Claudiu Moise
  • 376
  • 3
  • 14
5

I got the answer of this question. For this I have used Mappings JSON in which I have added values like If Selected value is DEV then use dev, If QA then qa like this, and used below JSON which used Fn:FindInMap

[ { "Fn::FindInMap": [ "Enviroment", "PlatformName", { "Ref": "selectedEnv" } ] }, "clientname" ]

Below is the Mappings JSON:

"Mappings" : { "Enviroment" : { "PlatformName" : { "DEV" : "dev", "QA" : "qa", "UAT" : "uat", "PROD" : "prod" } } }

Vikramsinh Gaikwad
  • 827
  • 1
  • 9
  • 23
  • 4
    I too have done like this, but really wish they would prove a function we can use for these simple manipulations. I've been resorting to doing these types of manipulations in Jenkins Shell before sending to CloudFormation. Shell Example: UpperVar=DEV, LowerVar=${UpperVar,,} – wheeleruniverse Apr 03 '20 at 20:57
2

You can do this with a CloudFormation macro.

Parameters:
  InputString:
    Default: "This is a test input string"
    Type: String
Resources:
  S3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      Tags:
        - Key: Upper
          Value:
            'Fn::Transform':
             - Name: 'String'
               Parameters:
                 InputString: !Ref InputString
                 Operation: Upper

https://github.com/awslabs/aws-cloudformation-templates/tree/master/aws/services/CloudFormation/MacrosExamples/StringFunctions

Below is from the AWS Documentation


How AWS CloudFormation macros work

There are two major steps to processing templates using macros: creating the macro itself, and then using the macro to perform processing on your templates.

To create a macro definition, you need to create the following:

  • An AWS Lambda function to perform the template processing. This Lambda function accepts either a snippet or an entire template, and any additional parameters that you define. It returns the processed template snippet or the entire template as a response.

  • A resource of type AWS::CloudFormation::Macro, which enables users to call the Lambda function from within AWS CloudFormation templates. This resource specifies the ARN of the Lambda function to invoke for this macro, and additional optional properties to assist with debugging. To create this resource within an account, author a stack template that includes a AWS::CloudFormation::Macro resource, and then create a stack from the template.

To use a macro, reference the macro in your template:

  • To process a section, or snippet, of a template, reference the macro in a Fn::Transform function located relative to the template content you want to transform. When using Fn::Transform, you can also pass any specified parameters it requires.

  • To process an entire template, reference the macro in the Transform section of the template.

Next, you typically create a change set and then execute it. (Processing macros can add multiple resources that you might not be aware of. To ensure that you're aware of all of the changes introduced by macros, we strongly advise that you use change sets.) AWS CloudFormation passes the specified template content, along with any additional specified parameters, to the Lambda function specified in the macro resource. The Lambda function returns the processed template content, be it a snippet or an entire template.

After all macros in the template have been called, AWS CloudFormation generates a change set that includes the processed template content. After you review the change set, execute it to apply the changes.


FOR EXAMPLE:

AWSTemplateFormatVersion: 2010-09-09
Resources:
  TransformExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: [lambda.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: ['logs:*']
                Resource: 'arn:aws:logs:*:*:*'
  TransformFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import traceback
          def handler(event, context):
              response = {
                  "requestId": event["requestId"],
                  "status": "success"
              }
              try:
                  operation = event["params"]["Operation"]
                  input = event["params"]["InputString"]
                  no_param_string_funcs = ["Upper", "Lower", "Capitalize", "Title", "SwapCase"]
                  if operation in no_param_string_funcs:
                      response["fragment"] = getattr(input, operation.lower())()
                  elif operation == "Strip":
                      chars = None
                      if "Chars" in event["params"]:
                          chars = event["params"]["Chars"]
                      response["fragment"] = input.strip(chars)
                  elif operation == "Replace":
                      old = event["params"]["Old"]
                      new = event["params"]["New"]
                      response["fragment"] = input.replace(old, new)
                  elif operation == "MaxLength":
                      length = int(event["params"]["Length"])
                      if len(input) <= length:
                          response["fragment"] = input
                      elif "StripFrom" in event["params"]:
                          if event["params"]["StripFrom"] == "Left":
                              response["fragment"] = input[len(input)-length:]
                          elif event["params"]["StripFrom"] != "Right":
                              response["status"] = "failure"
                      else:
                          response["fragment"] = input[:length]
                  else:
                      response["status"] = "failure"
              except Exception as e:
                  traceback.print_exc()
                  response["status"] = "failure"
                  response["errorMessage"] = str(e)
              return response
      Handler: index.handler
      Runtime: python3.6
      Role: !GetAtt TransformExecutionRole.Arn
  TransformFunctionPermissions:
    Type: AWS::Lambda::Permission
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !GetAtt TransformFunction.Arn
      Principal: 'cloudformation.amazonaws.com'
  Transform:
    Type: AWS::CloudFormation::Macro
    Properties:
      Name: 'String'
      Description: Provides various string processing functions
      FunctionName: !GetAtt TransformFunction.Arn
Mr. Young
  • 2,364
  • 3
  • 25
  • 41
  • 5
    It's not clear from the README posted, but you need to add that Python file as a transform function rather than just using the upper function. CF doesn't have any built-in support for this and you need to add your own transform function to do it. – shanet Jun 25 '20 at 00:22
  • 1
    @shanet this is a cloudformation macro that you will deploy as a lambda. More information on CloudFormation macros can be found here. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-macros.html – Mr. Young Jun 26 '20 at 01:16
  • 7
    Yes, that's the point I'm making. The answer above and README from the github link look like this is syntax that can just be used to someone new to CF when you actually need to add the Python code from the linked example as a macro/lambda/what have you in order to use it. – shanet Jun 26 '20 at 08:58
  • I'm not sure how you arrived at that conclusion. The docs clearly state "There are two major steps to processing templates using macros: creating the macro itself, and then using the macro to perform processing on your templates." ... "To create a macro definition, you need to create the following:" ... "An AWS Lambda function to perform the template processing." ... "A resource of type `AWS::CloudFormation::Macro`, which enables users to call the Lambda function from within AWS CloudFormation templates." – Mr. Young Jul 14 '20 at 23:07
  • 3
    Which docs? There's nothing in your answer or the linked to GitHub repo that states "There are two major steps to processing templates using macros...". For someone that is new to CloudFormation I'd assume by "macro" it was something built-in that I could just use in my template. That's not the case, I need to define the macro myself first. – shanet Jul 16 '20 at 00:04
2

The accepted answer suggested using a CloudFormation macro, and another answer suggesting using FindInMap.

FindInMap is not very useful here, since it would only work with hardcoded values.

The macro suggestion will work, but requires quite a bit of setup (declare the macro in a separate stack, ensure your deployer role has permission to invoke the Lambda, and your CloudFormation stack is deployed with CAPABILITY_AUTO_EXPAND, and so on).

Declaring a custom resource within the template will work and IMO involves less work than relying on the macro. Here's a CFN snippet, adapting the S3 bucket resource you were asking about, demonstrating the use of a custom resource which will lowercase an arbitrary S3 bucket name:

  # Custom resource to transform input to lowercase.                                             
  LowerCaseLambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Description: Returns the lowercase version of a string
      MemorySize: 256
      Runtime: python3.8
      Handler: index.lambda_handler
      Role: !GetAtt LowerCaseLambdaRole.Arn
      Timeout: 30
      Code:
        ZipFile: |
          import cfnresponse

          def lambda_handler(event, context):                                                    
              output = event['ResourceProperties'].get('InputString', '').lower()                
              responseData = {'OutputString': output}                                            
              cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)                

  LowerCaseLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: "lambda-write-logs"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*"


  S3BucketName:
    Type: Custom::Lowercase
    Properties:
      ServiceToken: !GetAtt LowerCaseLambda.Arn
      InputString: !Ref selectedEnv

  S3Bucket:
    BucketName: !Join
      - ''
      - - !GetAtt S3BucketName.OutputString
        - "-deployment.companyname.com"
Josh Kupershmidt
  • 2,540
  • 21
  • 30
0

Simply you can use: Fn::Transform JSON:

{
    "Fn::Transform": {
        "Name": "macro name",
        "Parameters": {
            "Key": "value"
        }
    }
}

YAML:

Fn::Transform:
  Name : macro name
  Parameters :
    Key : value

Ref: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-transform.html

Haribk
  • 131
  • 7