aws-cloudformation

CloudFormation Multiline Substitution in YAML. Is it possible?


I am trying to do a multiline substitution in a CFT and it's just not happening. The error I am getting is

An error occurred (ValidationError) when calling the ValidateTemplate operation: Template format error: unsupported structure.

Which is quite non-descript. I have the CFT plugin for intellij and it isn't giving me any syntax errors. Is such a thing supported? The problem line is at Fn::Sub

According to this documentation it is.

Here is the sample I am working with. I have the whole CFT working with hardcoded values but I would like it working with imported values from the CFT that created the parts of the stack that I am trying to watch

code:

AWSTemplateFormatVersion: 2010-09-09
Description: "Per ticket: CLOUD-1284"
Parameters:
  LogGroupName:
    Type: String
    Default: "ct/dev-logs"
    AllowedValues: ["ct/dev-logs","ct/prod-logs"]
    Description: Enter CloudWatch Logs log group name. Default is ct/dev-logs

  Email:
    Type: String
    Description: Email address to notify when an API activity has triggered an alarm
    Default: cloudops@
Resources:
  PolicyUpdates:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern:
        Fn::Sub:
        - >-
        { ($.eventSource = iam.amazonaws.com) &&
          (($.eventName = Update*) || ($.eventName = Attach*) || ($.eventName = Delete*) || ($.eventName = Detach*) ||($.eventName = Put*)) &&
          (($.requestParameters.roleName = ${Ec2Role}) || ($.requestParameters.roleName = ${RdsRole})) }
        - Ec2Role: !ImportValue infra-Ec2IamRole
        - RdsRole: !ImportValue infra-RdsIamRole

      LogGroupName: !Ref LogGroupName
      MetricTransformations:
      - MetricValue: 1
        MetricNamespace: SpecialMetrics
        MetricName: PolicyUpdateMetrics

  PolicyUpdatesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: " Policies have have been updated"
      AlarmActions:
      - Ref: AlarmNotificationTopic
      MetricName: PolicyUpdateMetrics
      Namespace: SpecialMetrics
      Statistic: Sum
      Period: 10
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: notBreaching

  S3BucketPolicyUpdates:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: >-
        { ($.eventSource = s3.amazonaws.com) && (($.eventName = Put*) || ($.eventName = Delete*)) &&
        (($.requestParameters.bucketName = assets-us-east-1) || ($.requestParameters.bucketName = logs-us-east-1)) }
      LogGroupName: !Ref LogGroupName
      MetricTransformations:
      - MetricValue: 1
        MetricNamespace: SpecialMetrics
        MetricName: S3BucketPolicyUpdateMetric

  S3BucketPolicyUpdatesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: " S3 bucket security settings have been updated"
      AlarmActions:
      - Ref: AlarmNotificationTopic
      MetricName: S3BucketPolicyUpdateMetric
      Namespace: SpecialMetrics
      Statistic: Sum
      Period: 10
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: notBreaching

  AlarmNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
      - Endpoint: !Ref Email
        Protocol: email

Solution

  • Yes, but you just need to fix the syntax.


    The fix:

    Here's a simplified version of your code showing corrected syntax:

    ---
    AWSTemplateFormatVersion: 2010-09-09
    Description: Test Stack
    Resources:
      PolicyUpdates:
        Type: AWS::Logs::MetricFilter
        Properties:
          FilterPattern:
            Fn::Sub:
            - >-
              { ($.eventSource = iam.amazonaws.com) &&
              (($.eventName = Update*) || ($.eventName = Attach*) || ($.eventName = Delete*) || ($.eventName = Detach*) ||($.eventName = Put*)) &&
              (($.requestParameters.roleName = ${Ec2Role}) || ($.requestParameters.roleName = ${RdsRole})) }
            - {Ec2Role: MyEc2Role, RdsRole: MyRdsRole}
          LogGroupName: !Ref LogGroup
          MetricTransformations:
          - MetricValue: 1
            MetricNamespace: SpecialMetrics
            MetricName: PolicyUpdateMetrics
    
      LogGroup:
        Type: AWS::Logs::LogGroup
    

    On creating that stack the following Metric Filter is created:

    ▶ aws logs describe-metric-filters --query 'metricFilters[].filterPattern' 
    [
        "{ ($.eventSource = iam.amazonaws.com) && (($.eventName = Update*) || ($.eventName = Attach*) || ($.eventName = Delete*) || ($.eventName = Detach*) ||($.eventName = Put*)) && (($.requestParameters.roleName = MyEc2Role) || ($.requestParameters.roleName = MyRdsRole)) }"
    ]
    

    Thus, you would need to change your Fn::Sub to:

    FilterPattern:
      Fn::Sub:
        - >-
          { ($.eventSource = iam.amazonaws.com) &&
            (($.eventName = Update*) || ($.eventName = Attach*) || ($.eventName = Delete*) || ($.eventName = Detach*) ||($.eventName = Put*)) &&
            (($.requestParameters.roleName = ${Ec2Role}) || ($.requestParameters.roleName = ${RdsRole})) }
        - {Ec2Role: !ImportValue infra-Ec2IamRole, RdsRole: !ImportValue infra-RdsIamRole}
    

    How to get better error messages:

    The first thing I did was run cloudformation validate-template:

    ▶ aws cloudformation validate-template --template-body file://cloudformation.yml
    
    An error occurred (ValidationError) when calling the ValidateTemplate operation:
      Template format error: YAML not well-formed. (line 23, column 45)                    
    

    Since it's a YAML formatting issue, the yamllint utility usually provides more information:

    ▶ yamllint cloudformation.yml 
    cloudformation.yml
      23:45     error    syntax error: could not find expected ':'
    

    Going into the vim editor and issuing a command:

    :cal cursor(23,45)
    

    Takes me to line 23, column 45 where I find the beginning of the string ${Ec2Role}.

    The first problem I see is that the indenting is wrong. That's actually the cause of that message.

    By indenting lines 21-23 by 2 more spaces makes the template valid YAML. Then I got a more helpful response from cloudformation validate-template:

    ▶ aws cloudformation validate-template --template-body file://cloudformation.yml 
    
    An error occurred (ValidationError) when calling the ValidateTemplate operation:
      Template error: One or more Fn::Sub intrinsic functions don't specify expected
      arguments. Specify a string as first argument, and an optional second argument
      to specify a mapping of values to replace in the string
    

    At this point, it can be seen from the documentation that the call to Fn::Sub is syntactically wrong.