My CloudFormation conditions are not being evaluated correctly when I set multiple conditions on resources.
I've created a modular CodePipeline template to allow deploying with or without a database. I've placed multiple conditions on some key resources.
Conditions:
HasDatabase: !Equals [ !Ref HasDatabase, true ]
IsECS: !Equals [ !Ref IsECS, true ]
Resources:
MyFakeBucket:
Type: AWS::S3::Bucket
Condition: IsECS
Condition: HasDatabase
I'm expecting MyFakeBucket to be created when BOTH conditions evaluate to true, however it's created when EITHER are.
My solution was to create new conditions that were combinations on existing ones:
Conditions:
HasDatabase: !Equals [ !Ref HasDatabase, true ]
IsECS: !Equals [ !Ref IsECS, true ]
ECSNoDB: !And
- !Condition NoDatabase
- !Condition IsECS
ECSDB: !And
- !Condition HasDatabase
- !Condition IsECS
Now my resources looks like this:
Resources:
MyFakeBucket:
Type: AWS::S3::Bucket
Condition: ECSDB
Related
I currently have this cloud formation template that is supposed to create an event bridge resource with all the necessary configurations, but I can't get it to create because I can't get cloud formation to verify if a key exists in the secrets manager or not.
to be more clear, I want my event-bridge.yml template resource to only be created if my key ${Stage}/${SubDomain}/django-events-api-key is already defined in the secrets manager and has a valid value(meaning it does not only have an empty string or a AWS::NoValue); this because I need to create the key after the stack is created and deployed, before the stack is not deployed, so I can't execute my command to generate the key
I have this:
event-bridge.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Event scheduling for sending the email digest"
Parameters:
SubDomain:
Description: The part of a website address before your DomainName - e.g. www or img
Type: String
DomainName:
Description: The part of a website address after your SubDomain - e.g. example.com
Type: String
Stage:
Description: Stage name (e.g. dev, test, prod)
Type: String
DjangoApiKey:
Description: Api key for events bridge communication
Type: String
Conditions:
DjangoApiKeyExists: !Or [ !Not [ !Equals [ !Ref DjangoApiKey, !Ref AWS::NoValue ] ], !Not [ !Equals [ !Ref DjangoApiKey, "" ] ] ]
Outputs:
DjangoEventsConnection:
Description: Connection to the Django backend for Event Bridge
Value: !Ref DjangoEventsConnection
Resources:
MessageDigestEventsRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${SubDomain}-chat-digest"
Description: "Send out email digests for a chat"
ScheduleExpression: "rate(15 minutes)"
State: "ENABLED"
Targets:
- Arn: !GetAtt MessageDigestEventsApiDestination.Arn
HttpParameters:
HeaderParameters: { }
QueryStringParameters: { }
Id: !Sub "${SubDomain}-chat-digest-api-target"
RoleArn: !GetAtt MessageDigestEventsRole.Arn
EventBusName: "default"
DjangoEventsConnection:
Type: AWS::Events::Connection
Properties:
Name: !Sub "${SubDomain}-django"
AuthorizationType: "API_KEY"
AuthParameters:
ApiKeyAuthParameters:
ApiKeyName: "Authorization"
ApiKeyValue: !Ref DjangoApiKey
in a main.yml template I pass the key variable like this:
EventBridge:
DependsOn: [ VpcStack, DjangoEventBridgeApiKey ]
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./event-bridge.yaml
Parameters:
SubDomain: !Ref SubDomain
DomainName: !Ref DomainName
Stage: !Ref Stage
DjangoApiKey: !Sub '{{resolve:secretsmanager:arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SubDomain}/django-events-api-key}}' <--
but that will always fail because the key is not defined, I would like to pass an empty string or something I can use as a conditional
I have also tried defining the secret, so it exists:
Resources:
DjangoEventBridgeApiKey:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${Stage}/${SubDomain}/django-events-api-key
Description: !Sub Credentials for the event bridge integration https://api.${SubDomain}.circular.co
Tags:
- Key: Name
Value: django-events-api-key
EventBridge:
DependsOn: [ VpcStack, DjangoEventBridgeApiKey ]
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./event-bridge.yaml
Parameters:
SubDomain: !Ref SubDomain
DomainName: !Ref DomainName
Stage: !Ref Stage
DjangoApiKey: !Sub '{{resolve:secretsmanager:${DjangoEventBridgeApiKey}}}'
But that for some reason, is still making my condition above fail, making the stack attempt to execute, I still cant figure out why is my condition not working
Any ideas on how to make this better? any help provided will be super useful to me
Okay found out the biggest problem with my implementation, on event-bridge.yml:
AWSTemplateFormatVersion: "2010-09-09"
Description: "Event scheduling for sending the email digest of chat messages"
Parameters:
SubDomain:
Description: The part of a website address before your DomainName - e.g. www or img
Type: String
DomainName:
Description: The part of a website address after your SubDomain - e.g. example.com
Type: String
Stage:
Description: Stage name (e.g. dev, test, prod)
Type: String
DjangoApiKey:
Description: Api key for events bridge communication
Type: String
Conditions:
DjangoApiKeyExists: !Not [ !Equals [ !Ref DjangoApiKey, "" ] ] # <-- this works
Outputs:
DjangoEventsConnection:
Condition: DjangoApiKeyExists
Description: Connection to the django backend for Event Bridge
Value: !Ref DjangoEventsConnection
Resources:
DjangoEventsConnection:
Type: AWS::Events::Connection
Condition: DjangoApiKeyExists
Properties:
Name: !Sub "${SubDomain}-django"
AuthorizationType: "API_KEY"
AuthParameters:
ApiKeyAuthParameters:
ApiKeyName: "Authorization"
ApiKeyValue: !Ref DjangoApiKey
# This does not update when we change the secret - so we need to force an update - need a more permanent solution
# ApiKeyValue: pop
MessageDigestEventsApiDestination:
Type: AWS::Events::ApiDestination
Condition: DjangoApiKeyExists
DependsOn: DjangoEventsConnection
on main.yml
...
DjangoEventBridgeApiKey:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${Stage}/${SubDomain}/django-events-api-key # <- this was missing ${Stage}
SecretString: " "
Tags:
- Key: Name
Value: django-events-api-key
EventBridge:
DependsOn: DjangoEventBridgeApiKey
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./event-bridge.yaml
Parameters:
SubDomain: !Ref SubDomain
DomainName: !Ref DomainName
Stage: !Ref Stage
DjangoApiKey: !Sub '{{resolve:secretsmanager:arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Stage}/${SubDomain}/django-events-api-key}}'
Tags:
- Key: Stage
Value: !Ref Stage
- Key: SubDomain
Value: !Ref SubDomain
- Key: SecretKeyName
Value: !Ref DjangoEventBridgeApiKey
I have the following resource in my CloudFormation template that's trying to create a listener rule. The idea is, based on the passed-in EnvironmentType, and the AWS Region, I want to import the listener ARN from the appropriate CloudFormation stack that exported it.
Parameters:
EnvironmentType:
Type: String
Default: "sandbox"
ECSClusterStackNameParameter:
Type: String
Default: "ECS-US-Sandbox"
Mappings:
production:
us-east-1:
stackWithAlbListenerInfo: "ECS-US-Prod"
eu-north-1:
stackWithAlbListenerInfo: "ECS-EU-Prod"
staging:
us-east-1:
stackWithAlbListenerInfo: "ECS-US-Staging"
eu-north-1:
stackWithAlbListenerInfo: ""
sandbox:
us-east-1:
stackWithAlbListenerInfo: "ECS-US-Sandbox"
eu-north-1:
stackWithAlbListenerInfo: ""
Conditions:
StackExists:
!Not [ !Equals [ !FindInMap [ !Ref EnvironmentType, !Ref "AWS::Region", stackWithAlbListenerInfo ], ""] ]
Resources:
AlbListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: UseListenerRule
Properties:
ListenerArn:
!If
- StackExists
-
- Fn::ImportValue:
!Join
- "-"
- - !FindInMap [ !Ref EnvironmentType, !Ref "AWS::Region", stackWithAlbListenerInfo ]
- "ListenerArn"
- Fn::ImportValue:
- !Sub "${ECSClusterStackNameParameter}-ListenerArn"
However, it fails validation due to this error, and seems like the first Fn::ImportValue: doesn't like the !Join. But !Join returns a concatenated string correct? What am I missing?
ERROR: Service: marcom-stats-service, cfnUpdate error: com.amazonaws.services.cloudformation.model.AmazonCloudFormationException: Template error: the attribute in Fn::ImportValue must be a string or a function that returns a string (Service: AmazonCloudFormation; Status Code: 400; Error Code: ValidationError; Request ID: 2678552e-cf6c-46e1-b640-a7c07de385c2; Proxy: null)
UPDATE:
Though Robert Kossendey's answer fixed my error, my original template was wrong. This is really what I wanted to do. I hope it helps someone.
AlbListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: UseListenerRule
Properties:
ListenerArn:
Fn::ImportValue: !Sub
- ${StackName}-ListenerArn
- { StackName: !If [ StackExists, !FindInMap [ !Ref EnvironmentType, !Ref "AWS::Region", stackWithAlbListenerInfo ], !Ref ECSClusterStackNameParameter ] }
Now I see it I think. At the second import you need to remove the dash in front of the !Sub:
- Fn::ImportValue:
!Sub "${ECSClusterStackNameParameter}-ListenerArn"
I have next CloudFormation file:
Mappings:
MyMap:
us-east-1:
Roles:
- "roleA"
- "roleB"
...
Resources:
MyPolicy:
Type: "AWS::IAM::Policy"
PolicyDocument:
Statement:
- Effect: "Allow"
Action:
- "sts:AssumeRole"
Resource:
Fn::FindInMap: ["MyMap", !Ref AWS::Region, "Roles"]
Everything works fine, however now I need to add an extra role that would be defined for all regions, however simply adding additional role to Resource: section doesn't work, since it fails with template syntax error.
Is there a way to combine list of results from FindInMap and another item? Something like:
Resource:
Fn::FindInMap: ["MyMap", !Ref AWS::Region, "Roles"]
- "roleC"
Yes, you can, but it won't be pretty:
Resource:
Fn::Split:
- ','
- Fn::Join:
- ','
- - !Join [',', !FindInMap ["MyMap", !Ref "AWS::Region", "Roles"]]
- 'roleC'
Basically, first you join the MyMap list into a string, then you add roleC to the string, and then split it into List of Strings.
Based on the environment, I am trying to set the URL for a variable: It staging my URL should be https://staging.DNHostedZoneName , if prod - it should just be https://DNSHostedZoneName:
Here's my condition:
Conditions:
IsEnvProd: Fn::Equals [ !Ref Env, 'prod' ]
IsEnvStage: Fn::Equals [ !Ref Env, 'stage' ]
Here's where its been evaluated:
Environment:
- Name: NODE_ENV
Value: !Ref NodeEnv
- Fn::If:
- IsEnvStage
- Name: CORE_URL
Value:
Fn::Join:
- ""
- - "https://"
- "staging"
- "."
- !Ref DnsHostedZoneName
- Name: NCVCORE_URL
Value:
Fn::Join:
- ""
- - "https://"
- !Ref DnsHostedZoneName
I am getting the following error:
Template format error: Conditions can only be boolean operations on parameters and other conditions
Without the full template, it is difficult to try to recreate the issue, but here your snippets refactored with a possible error removed.
Adjusted the conditionals to use all shorthand.
Conditions:
IsEnvProd: !Equals [!Ref "Env", "prod"]
IsEnvStage: !Equals [!Ref "Env", "stage"]
There was an additional space in the YAML so that has been removed, and reformatted.
Environment:
- Name: "NODE_ENV"
Value: !Ref "NodeEnv"
- !If
- "IsEnvStage"
- Name: "CORE_URL"
Value: !Sub "https://staging.${DnsHostedZoneName}"
- Name: "NCVCORE_URL"
Value: !Sub "https://${DnsHostedZoneName}"
Usually the conditions defined are used as an attribute to an aws resource and you specify the name of the condition as a value. You can try https://krunal4amity.github.io - it is an online cloudformation template generator. It takes away a lot of such dreadful work.
You can try to orchestrate creation of specific resources using AWS::NoValue
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html
Below is taken from variables creation for LambdaFunction
Conditions:
IsProd: !Equals [!Ref Env, "production"]
Environment:
Variables:
USER: !If [IsProd, !GetAtt ...., Ref: AWS::NoValue]
Is there a possibility in CFN templates to add some specific Security Groups to ALB depending on the parameter?
I have a situation where two security groups are adding to the ALB:
ALB
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
...
SecurityGroups:
- !Ref 'SecurityGroup1'
- !Ref 'SecurityGroup2'
Now there is a SecurityGroup3 that I would like to eventually add only if some parameter has a specific value. Let's say if parameter add_sg3 equals yes then the third SG is added to ALB. I always use "!If in similar situations but there are more than 2 SGs. Any advice would be welcome. Thanks!
You can achieve that using a Condition and the AWS::NoValue pseudo-parameter. Follow below a complete example:
Parameters:
Environment:
Type: String
Default: dev
AllowedValues: ["dev", "prod"]
VpcId:
Type: 'AWS::EC2::VPC::Id'
Subnet1:
Type: 'AWS::EC2::Subnet::Id'
Subnet2:
Type: 'AWS::EC2::Subnet::Id'
Conditions:
MyTest: !Equals ["dev", !Ref Environment]
Resources:
ALB:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
SecurityGroups:
- !Ref SecurityGroup1
- !If [ MyTest, !Ref SecurityGroup2, !Ref 'AWS::NoValue' ]
Subnets:
- !Ref Subnet1
- !Ref Subnet2
SecurityGroup1:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'Group 1'
VpcId: !Ref VpcId
SecurityGroup2:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'Group 2'
VpcId: !Ref VpcId