AWS Cloudformation - use AWS::NoValue within Fn::Sub - amazon-web-services

I am using cloudformation and I want to be able to use the pseudo value
AWS::NoValue within the Fn::Sub like this:
!Sub ["ATL_DATASET_URL=${DatasetURL}",
DatasetURL: !If [IsURLProvided,
!Ref BitbucketDatasetURL,
!Ref "AWS::NoValue"]]
My Template passes validation but does not deploy. Here is the error message I get when I click Create Stack.
Template error: every value of the context object of every Fn::Sub object must be a string or a function that returns a string

If you want to skip setting a value for DatasetURL, make the !If to return an empty string '' when the condition evaluates to false instead of AWS::NoValue.
Returning AWS::NoValue when false, removes the mapping for DatasetURL.

The alternative to #franklinsijo is to swap the If and Sub statements if you want to actually remove the property (e.g. YourPropertyName) if BitbucketDatasetURL is not given.
YourPropertyName: !If
- IsURLProvided
- !Sub ["ATL_DATASET_URL=${DatasetURL}", DatasetURL: !Ref BitbucketDatasetURL]
- !Ref "AWS::NoValue"
Or shorter
YourPropertyName: !If
- IsURLProvided
- !Sub "ATL_DATASET_URL=${BitbucketDatasetURL}"
- !Ref "AWS::NoValue"

Related

CloudFormation Conditions Problems

I have a CloudFormation template and I want to use a condition within the AutoScale structure, but I'm getting errors. I wonder if I'm missing something?
My condition:
Conditions:
CreateLBResources: !Equals
- !Ref LB
- true
Load Balancer in AutoScale:
LoadBalancerNames:
!If [CreateLBResources, !Ref LoadBalancer, !Ref "AWS::NoValue"]
Error:
Value of property LoadBalancerNames must be of type List of String
What do I want to do?
If I enter "true", add the name of the loadbalancer, if I enter "false" then leave it blank.
Thanks for helps.
In the LoadBalancerNames you don't need any !If condition. You can simply use Condition in ASG block.
myASG:
Condition: CreateLBResources
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LoadBalancerNames: [<LBName>]
...
On the basis of condition CreateLBResources, it will do the job(create or do not create ASG).
I solved my problem.
LoadBalancerNames:
- !If [CreateLBResources, !Ref LoadBalancer, !Ref "AWS::NoValue"]
I forgot to add the - character.
Thanks.

Split string loaded from SecretsManager

I am trying to create a ListenerRule that would accept only certain IP addresses loaded from Secrets Manager. Here's my original code:
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
Fn::If:
- IsProdEnvironment
- - Field: host-header
Values:
- !Ref 'BaseUrl'
- - Field: host-header
Values:
- !Ref 'BaseUrl'
- Field: source-ip
SourceIpConfig:
Values:
- !Split [ ';', !Sub '{{resolve:secretsmanager:/${ProjectName}/${EnvType}/${ServiceName}:SecretString:ALLOWED_IPS}}' ]
Resolve above seems to work properly and is resolved to a string like 1.2.3.4/32;2.3.4.5/32;4.5.6.7/29;5.6.7.8/32 (addresses obviously obfuscated but the amount and subnet mask is the same).
However when I try to deploy the template I get an error:
The specified value '1.2.3.4/32;2.3.4.5/32;4.5.6.7/29;5.6.7.8/32' is not a valid CIDR block (Service: AmazonElasticLoadBalancingV2; Status Code: 400; Error Code: ValidationError; Request ID: error-id)
This error by itself suggests that !Split does something because CloudFormation does not report error Value of property Values must be of type List of String. Clearly string was converted to a list containing one element but it was not split by ; character into list of 4 strings.
Also, when I try to run very similar code but with hardcoded value instead of secret it works fine:
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
Fn::If:
- IsProdEnvironment
- - Field: host-header
Values:
- !Ref 'BaseUrl'
- - Field: host-header
Values:
- !Ref 'BaseUrl'
- Field: source-ip
SourceIpConfig:
Values:
!Split [';', '1.2.3.4/32;2.3.4.5/32;4.5.6.7/29;5.6.7.8/32' ]
I also checked version without !Sub but with hardcoded path to Secret value:
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
Fn::If:
- IsProdEnvironment
- - Field: host-header
Values:
- !Ref 'BaseUrl'
- - Field: host-header
Values:
- !Ref 'BaseUrl'
- Field: source-ip
SourceIpConfig:
Values: !Split [';', '{{resolve:secretsmanager:/dummy-project/dev/foo:SecretString:ALLOWED_IPS}}']
As a result I get the same error as in first case: long string is not split into smaller parts.
To make this even weirder... this is a part of quite large template but when I copy this resource to a simple test.yaml that takes a list of Parameters required to generate SecretManager path it seems to be working properly...
What do I do wrong here?
I can't provide full answer as I agree #OleksiiDonoha and I belive that resolve:secretsmanager is resolves last, during change set creation, after all intrinsic functions are executed.
However, I would like to clarify the statement:
This error by itself suggests that !Split does something because CloudFormation does not report error Value of property Values must be of type List of String.
!Split uses only String, not list of strings, unlike Select. Also, if you provide wrong delimiter it will just return the original string.
So what is happening with Split, I think, is the following:
In the expression:
Values:
- !Split [ ';', !Sub '{{resolve:secretsmanager:/${ProjectName}/${EnvType}/${ServiceName}:SecretString:ALLOWED_IPS}}' ]
!Sub executes first and you end up with (for example):
Values:
- !Split [ ';', '{{resolve:secretsmanager:/MyProject/TestType/MyService:SecretString:ALLOWED_IPS}}' ]
Then !Split executes and attempts to split the string '{{resolve:secretsmanager:/MyProject/TestType/MyService:SecretString:ALLOWED_IPS}}' by ;. It can't, thus you end up with:
Values:
- '{{resolve:secretsmanager:/MyProject/TestType/MyService:SecretString:ALLOWED_IPS}}'
And finally now this gets resolved, resulting in:
Values:
- "1.2.3.4/32;2.3.4.5/32;4.5.6.7/29;5.6.7.8/32"
This would explain your error.
The only solution I see at present would to use custom resource which would get the secret before the intrinsic functions evaluate or through an inner stack.
Alternatively, you are chaining stacks (one stacks produces outputs which go into parameters of the next stack) you could resolve the secret in the first stack, and just pass the CIDRs string list as a parameter to the second stack.
Hope this helps.

AWS CloudFormation Error 'Value of property AlarmActions must be of type List of String'

i am trying to update my cf stack and encounter the following error on deploy:
'Value of property AlarmActions must be of type List of String'
this is the property AlarmActions:
AlarmActions:
- !Ref SparksTeamSNSTopic
- !If
- CreateProdResources
- - !Ref SparksProdAlarmSNSTopic
- !ImportValue
'Fn::Sub': '${Environment}-BMCMajorAlarmTopic'
- - !Ref 'AWS::NoValue'
As per the AWS documentation, AlarmActions property must contain values, as a list of strings. So you should have something like this if it's JSON:
"AlarmActions":[
{"Ref":"ARN of something"},
{"Ref":"ARN of something"}
]
But since you've used YAML you should be having something like this:
AlarmActions:
- !Split [",", !Ref SparksTeamSNSTopic] <-- make sure SparksTeamSNSTopic contains a list of strings; hence this will split it by comma
You may define the SparksTeamSNSTopic as
"SparksTeamSNSTopic" : ["topicarn1", "topicarn2"]
Try this,
AlarmActions:
- !Ref SparksTeamSNSTopic
- !If
- CreateProdResources
- - !Ref SparksProdAlarmSNSTopic
- !ImportValue
'Fn::Sub': '${Environment}-BMCMajorAlarmTopic'
- !Ref 'AWS::NoValue'

Cloudformation: Using Fn::Join and Fn:GetAtt together

I am trying to define value in one of the attributes for Type: AWS::Lambda::EventSourceMapping
Below is my snippet (latest attempt):
FunctionName: #trigger the echo function previously defined
"Fn::Join": [":", [{"Fn::GetAtt" : ["LambdaName", "Arn"]}, "live" ]]
My cloud formation stack, however fails to deploy with following error:
"StatusReason": "Template error: every Fn::Join object requires two parameters, (1) a string delimiter and (2) a list of strings to be joined or a function that returns a list of strings (such as Fn::GetAZs) to be joined."
I have tried couple of variations with the brackets, however keep getting same error. What am I missing in the syntax?
P.S. I am defining this in a yaml file
My first question is whether LambdaName references something that can be used with GetAtt to provide an ARN. Otherwise, it could just be a formatting issue. I'm not sure AWS CFN can read the embedded curly brackets you're using to wrap the Fn::GetAtt.
Maybe one of these would work better?
FunctionName:
Fn::Join:
- ':'
- - Fn::GetAtt:
- LambdaName
- Arn
- "live"
Or
FunctionName: !Join [':', [!GetAtt LambdaName.Arn, 'live]]
LambdaSourceMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
Enabled: 'true'
EventSourceArn: <SQS ARN> or <Kinesis ARN> or <DynamoDb ARN>
FunctionName:
Fn::Join:
- ':'
- - Fn::GetAtt:
- LambdaName
- Arn
- 'live'

CloudFormation: How to use AWS::AccountId in Mappings?

I have a mapping that looks like this:
Mappings:
AccountToParams:
aws-1234567890:
sshSecurityGroup: sg-abcedf12
And I'd like to retrieve my variables by AccountId, but this doesn't get past the "validation" step
SecurityGroups:
- !FindInMap [AccountToParams, !Sub "aws-${AWS::AccountId}", sshSecurityGroup]
Error is
16/08/2017, 16:36:18 - Template contains errors.: Template error:
every Fn::FindInMap object requires three parameters,
the map name, map key and the attribute for return value
The goal is to have some configuration driven by the account (hence environment) this is run under. And I can't seem to use the accountId as the key in the mapping, otherwise AWS isn't happy because it doesn't contain alphanumeric chars
Change the map to:
Mappings:
AccountToParams:
"1234567890":
sshSecurityGroup: sg-abcedf12
and use !Ref instead of !Sub:
SecurityGroupIds:
- !FindInMap [AccountToParams, !Ref "AWS::AccountId", sshSecurityGroup]
Use FN::Join to prepend "aws" string to account ID if that's required further down the stack.
This works
Mappings:
AccountToParams:
"123456789012":
sshSecurityGroup: sg-abcdef12
Resources:
SecurityGroups: !FindInMap
- AccountToParams
- !Ref 'AWS::AccountId'
- sshSecurityGroup