Split string loaded from SecretsManager - amazon-web-services

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.

Related

How to create list of resource ARNS using a CommaDelimitedList parameter?

I am trying to create a Cloudwatch event rule which will use multiple Codepipelines as source and triggers target.
Parameters:
SourcePipeline:
Description: Name of Source codepipeline
Type: CommaDelimitedList
Default: 'test3, test4'
Resources:
PipelineTrigger:
Type: 'AWS::Events::Rule'
Properties:
EventPattern:
source:
- aws.codepipeline
resources: !Split
- ','
- !Sub
- 'arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${pipeline}'
- pipeline: !Join
- ',arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:'
- !Ref SourcePipeline
Expecting resources as below:
"resources": ["arn:aws:codepipeline:us-east-1:123:test3","arn:aws:codepipeline:us-east-1:123:test4"],
Any idea how to pass the list of names as a parameter?
#FYI Reference i am following Using Lists of ARNs
First, it should be !Ref SourcePipelines. Second you forgot about comma. you can't do this the way you want. This is because, the first parameter to join must be literal string, not any CloudFormation expression or function. So you have to hardcode your account id and region:
resources: !Split
- ','
- !Sub
- 'arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${pipeline}'
- pipeline: !Join
- ',arn:aws:codepipeline:us-east-1:12312321:'
- !Ref SourcePipelines

AWS Cloudformation - use AWS::NoValue within Fn::Sub

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"

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'

How do I pass a list of strings as a parameter in CloudFormation?

I've got a nested CloudFormation template which accepts a number of parameters from its root template to configure it. At the moment I'm only passing simple string parameters but now I need to pass a list of S3 bucket ARNs onto the child template.
ChildLambdaStack:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
AwsRegion: !Ref AwsRegion
Environment: !Ref Environment
Product: !Ref Product
S3Buckets: "arn:aws:s3:::bucket1,arn:aws:s3:::bucket2"
TemplateURL: "https://s3.amazonaws.com/child-template.yml"
And then in the child template I have this
AWSTemplateFormatVersion: "2010-09-09"
Description: "Child Lambda"
Parameters:
AwsRegion:
Type: String
Environment:
Type: String
Product:
Type: String
S3Buckets:
Type: String
Resources:
DeployerPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:CreateBucket
- s3:DeleteBucket
- s3:ListBucket
- s3:PutBucketNotification
Resource:
- Fn::Split:
- ","
- !Ref S3Buckets
My idea is that that list of S3 bucket ARNs that I'm inputting is expanded in the child template like this
Resource:
- arn:aws:s3:::bucket1
- arn:aws:s3:::bucket2
But when I run the template in, it just errors out
Syntax errors in policy. (Service: AmazonIdentityManagement; Status Code: 400; Error Code: MalformedPolicyDocument)
I've tried other variations like using a CommaDelimitedList parameter type, but none work. Is there a simple way to pass in a list of strings as a parameter?
Because the return value of !Split is A list of string values. I would do it in the following way:
[...]
Resource: !Split [",", !Ref S3Buckets]
[...]
As #MaiKaY points out, the flaw in #Liam Mayfair's code is that Fn::Split is preceded by - which results in a list containing a single element which is a list. The fixed code would look like
...
Resource:
Fn::Split:
- ","
- !Ref S3Buckets
On a more general note, you must make sure to use a Parameter type of String not CommaDelimitedList when using Fn::Split as it won't split a CommaDelimitedList.
If you use CommaDelimitedList with Fn::Split you'll get the error Template error: every Fn::Split object requires two parameters, (1) a string delimiter and (2) a string to be split or a function that returns a string to be split
If you use CommaDelimitedList with no Fn::Split you'll get the error Syntax errors in policy
There is actually a much better way. You can use the type List<String> in your CloudFormation Parameters:
# ...
Parameters:
S3Buckets:
Type: List<String>
# ...
Then pass the S3 Bucket list just like you did as comma separated values:
# ...
ChildLambdaStack:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
AwsRegion: !Ref AwsRegion
Environment: !Ref Environment
Product: !Ref Product
# Next line will be interpreted as a list
S3Buckets: "arn:aws:s3:::bucket1,arn:aws:s3:::bucket2"
TemplateURL: "https://s3.amazonaws.com/child-template.yml"
# ...
You can then assume that the type of the referenced Parameter is a list. So instead of:
# ...
Resource: !Split [",", !Ref S3Buckets]
# ...
You can just use:
# ...
Resource: !Ref S3Buckets
# ...

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