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
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.