How can I use Fn::GetAttr only when the resource exist? - amazon-web-services

I'm making one main stack and two nested stack.
The first nested stack is a stack which creates Lambda.
And the second nested stack creates Apigateway.
When making the Apigateway I send some Lambda outputs to the Apigateway template.
But some Lambda Outputs don't get created in some conditions.
So when I use Fn::GetAttr to send some outputs to the Apigateway template. I get an error because the output doesn't exist.
[Lambda Template (Nested Stack)]
Because of the condition conditionNeedMock the MockServerArn output wasn't created.
...
Outputs:
MockServerArn:
Condition: conditionNeedMock
Description: lambdaMock function Arn
Value: !GetAttr lambdaMock.Arn
...
[Main Template]
LambdaStack.Ouputs.MockserverArn wasn't created so I get an error when using !GetAttr.
...
Resources:
LambdaStack:
Type: AWS::CloudFormation::Stack
...
ApiGatewayStack:
Type: AWS::CloudFormation::Stack
DependsOn: LambdaStack
Properties:
TemplateURL: apigateway.yml
TimeoutInMinutes: 20
Parameters:
paramMockServerArn: !GetAtt LambdaStack.Outputs.MockServerArn
...

The only way that could possibly work (without introducing custom resources) is if conditionNeedMock is also in the root stack:
paramMockServerArn: !If [conditionNeedMock, !GetAtt LambdaStack.Outputs.MockServerArn, !Ref "AWS::NoValue"]

Related

Template validation failed when using SAM syntax instead of CloudFormation syntax for step function

I have the following step function in my AWS SAM template, it was defined using the syntax in the documentation. I'm using intrinsic functions to get some pseudoparameters but something is going wrong with them.
SfdcOrderEventsStepFunction:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionSubstitutions:
Region: !Ref "AWS::Region"
AccountId: !Ref "AWS::AccountId"
EventBusPublishTarget: "order-events"
DefinitionUri: sfn-definition.asl.yml
Events:
EventBridgeEvent:
Type: EventBridgeRule
Properties:
EventBusName: sfdc-events
Pattern:
# TODO: Update pattern when the salesforce service is ready
source:
- prefix: salesforce.com
detail-type:
- Order
detail:
Payload__c:
attributes:
type:
- order
InputPath: $.detail
Name: sfdc-order-events
Role: !Sub 'arn:aws:iam::${AWS::AccountId}:role/stepfunction_execution_role'
Tracing:
Enabled: true
when I try to deploy it shows me the following error:
Resource template validation failed for resource
SfdcOrderEventsStepFunction as the template has invalid properties.
Please refer to the resource documentation to fix the template.
Properties validation failed for resource SfdcOrderEventsStepFunction
with message:
#/De finitionSubstitutions/ AccountId: 2 subschemas matched instead of one
#/DefinitionSubstitutions/AccountId: expected type: Boolean, found: String
At the end it deploys without problems. The step function and all of its components run as expected and without errors, but I wanted to know if there if something I can do to fix the template.

How to get resource out of the CfnStack?

I used the CfnInclude to import a cloudformation template to my cdk code:
template= _cfn_include.CfnInclude(self,
'template',
template_file = "aws-waf-security-automations.yaml")
After that I used the get_resource() to get one of the resources from this template:
waf = template.get_resource("WebACLStack")
The problem is that this resource is not of the type WafWebACL, it is of the type CfnStack. Inside the template this resource is being described in this way:
WebACLStack:
Type: 'AWS::CloudFormation::Stack'
DependsOn: CheckRequirements
Properties:
TemplateURL: !Sub
- 'https://${S3Bucket}.s3.amazonaws.com/${KeyPrefix}/aws-waf-security-automations-webacl.template'
-
S3Bucket: !FindInMap ["SourceCode", "General", "TemplateBucket"]
KeyPrefix: !FindInMap ["SourceCode", "General", "KeyPrefix"]
Parameters:
ActivateAWSManagedRulesParam: !Ref ActivateAWSManagedRulesParam ...
How can I get the WAFWebACL out of the CfnStack?
As I understood, the WAFWebACL is a nested stack inside the CfnStack.
Obs.: I got this template from this url: https://docs.aws.amazon.com/solutions/latest/aws-waf-security-automations/template.html
Obs2: The template outputs like this way:
WAFWebACL:
Description: AWS WAF WebACL
Value: !GetAtt WebACLStack.Outputs.WAFWebACL
So could I get the WAFWebACL resource from the imput?
You need to load the nested stack first:
from aws_cdk.aws_wafv2 import CfnWebACL
web_acl_stack = template.load_nested_stack("WebACLStack", template_file="aws-waf-security-automations-webacl.template")
waf: CfnWebACL = web_acl_stack.get_resource("WAFWebACL")
I think you also need to keep the stack template of the nested stack locally and use the correct path instead of just "aws-waf-security-automations-webacl.template" when loading the nested stack.
(I assume you are using Python)

Getting an ARN for an imported Kinesis stream

I have two nested Cloudformation stacks - the first template needs to define a Kinesis stream, the second needs to use a reference to that stream's ARN, as an argument to a further nested stack.
So it seems I need to "export" the stream from the first template, and "import" it into the second (following AWS docs on importing/exporting values across stacks) -
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html
My export code [truncated] looks like this -
Outputs:
MyKinesisStreamOutput:
Value:
Ref: MyKinesisStream
Export:
Name: my-kinesis-stream
Resources:
MyKinesisStream:
Properties:
Name:
Ref: AppName
ShardCount: 1
Type: AWS::Kinesis::Stream
Whilst my import code [truncated] looks like this -
MyNestedStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub "https://s3.${AWS::Region}.amazonaws.com/my-nested-stack.yaml"
Parameters:
AppName: my-nested-stack
KinesisStream:
Fn::GetAtt:
- Fn::ImportValue:
my-kinesis-stream
- Arn
But then I get the following Cloudformation error -
Template error: every Fn::GetAtt object requires two non-empty parameters, the resource name and the resource attribute
and suspect I am falling foul of this -
For the Fn::GetAtt logical resource name, you cannot use functions. You must specify a string that is a resource's logical ID.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html
Assuming I am exporting and importing the Kinesis stream correctly, how then am I supposed to get its Arn value ?
When you export something in Outputs, it's just a string, you can no longer GetAtt on it in the importing template.
What you need to do is to additionally export ARN:
Outputs:
MyKinesisStream:
Value: !Ref MyKinesisStream
Export:
Name: my-kinesis-stream
MyKinesisStreamArn:
Value: !GetAtt MyKinesisStream.Arn
Export:
Name: my-kinesis-stream-arn
I can think of two potential ways:
1) Fn::GetAtt:[!ImportValue my-kinesis-stream, Arn]
Sorry wasn't reading carefully enough
or what I would prefer
2) Directly export the required value as output: Value: !GetAtt MyKinesisStream.Arn
Hope that helps!

How to check if a parameter exists in Systems Manager from CloudFormation

Reading the AWS documentation here, I've found the following statement:
AWS::SSM::Parameter::Name
The name of a Systems Manager parameter key.
Use this parameter when you want to pass the parameter key. For example, you can use this type to validate that the parameter exists.
I wasn't able to make it work, every time I get: Parameter validation failed: parameter value ... for parameter name ... does not exist.
Shoud it be trying to resolve the parameter type AWS::SSM::Parameter::Name? Anyway, I kept searching and found another statement here:
The first one in the list is used to pass the name of the parameter key as-is. CloudFormation will not fetch the value stored against it. For example, you can use this type to validate that the parameter exists in Parameter Store.
I'm probably not understanding it correctly, so I would like to request an example on how to check if a parameter existis in Systems Manager from CloudFormation?
Use case:
My main region has all parameters stored on Systems Manager, but my second one (redundancy) has only a few. Consider as example not creating the Zone/RecorSet twice in each region. For that I use a condition, as shown bellow:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
MainRegion:
Type: String
Default: 'us-east-1'
PrivateFullyQualifiedDomainName:
Type: 'AWS::SSM::Parameter::Name'
Default: '/company/route53/private'
Conditions:
CreateUniqueResources: !Equals [ !Ref 'AWS::Region', !Ref MainRegion ]
Resources:
Route53Stack:
Type: 'AWS::CloudFormation::Stack'
Condition: CreateUniqueResources
Properties:
TemplateURL: [some s3 bucket]
Parameters:
PrivateDomainName: !Ref PrivateFullyQualifiedDomainName
Outputs:
TestParameterName:
# Outputs: /company/route53/private
Value: !Ref PrivateFullyQualifiedDomainName
And the nested stack:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
PrivateDomainName:
Type: 'AWS::SSM::Parameter::Value<String>'
Resources:
[sutff...]
Outputs:
TestParameterValue:
# Outputs WHEN it exists: supersecretdomain.company.com
Value: !Ref PrivateDomainName

Cloudformation & Parameter Store: How to select parameter for the environment

I want to read the URL of my database from parameter store in my CloudFormation template. This is easy enough for a single URL, but I can't figure out how to change the URL with different environments.
I have four environments (development, integration, pre-production and production) and their details are stored on four different paths in Parameter Store:
/database/dev/url
/database/int/url
/database/ppe/url
/database/prod/url
I now want to pick the correct Database URL when deploying via CloudFormation. How can I do this?
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- int
- ppe
- prod
DatabaseUrl:
Type: 'AWS::SSM::Parameter::Value<String>'
# Obviously the '+' operator here won't work - so what do I do?
Default: '/database/' + Environment + '/url'
This feature isn't as neat as one would wish. You have to actually pass name/path of each parameter that you want to look up from the parameter store.
Template:
AWSTemplateFormatVersion: 2010-09-09
Description: Example
Parameters:
BucketNameSuffix:
Type: AWS::SSM::Parameter::Value<String>
Default: /example/dev/BucketNameSuffix
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub parameter-store-example-${BucketNameSuffix}
If you don't pass any parameters to the template, BucketNameSuffix will be populated with the value stored at /example/dev/BucketNameSuffix. If you want to use, say, a prod value (pointed to by /example/prod/BucketNameSuffix), then you should specify value for parameter BucketNameSuffix, but instead of passing the actual value you should pass the alternative name of the parameter to use, so you'd pass /example/prod/BucketNameSuffix.
aws cloudformation update-stack --stack-name example-dev \
--template-body file://./example-stack.yml
aws cloudformation update-stack --stack-name example-prod \
--template-body file://./example-stack.yml \
--parameters ParameterKey=BucketNameSuffix,ParameterValue=/example/prod/BucketNameSuffix
A not so great AWS blog post on this: https://aws.amazon.com/blogs/mt/integrating-aws-cloudformation-with-aws-systems-manager-parameter-store/
Because passing million meaningless parameters seems stupid, I might actually generate an environment-specific template and set the right Default: in the generated template so for prod environment Default would be /example/prod/BucketNameSuffix and then I can update the prod stack without passing any parameters.
You can populate CloudFormation templates with parameters stored in AWS Systems Manager Parameter Store using Dynamic References
In this contrived example we make two lookups using resolve:ssm and replace the environment using !Join and !Sub
AWSTemplateFormatVersion: 2010-09-09
Parameters:
Environment:
Type: String
Default: prod
AllowedValues:
- prod
- staging
Resources:
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
Image: !Join
- ''
- - 'docker.io/bitnami/redis#'
- !Sub '{{resolve:ssm:/app/${Environment}/digest-sha/redis}}'
Environment:
- Name: DB_HOST
Value: !Sub '{{resolve:ssm:/app/${Environment}/db-host}}'
You can make use of Fn::Join here.
Here is some pseudo code.
You will have to take Environment as an parameter, which you are already doing.
Creating the required string in the resources where DatabaseUrl is required.
Resources :
Instance :
Type : 'AWS::Some::Resource'
Properties :
DatabaseURL : !Join [ "", [ "/database/", !Ref "Environment" , "/url ] ]
Hope this helps.
Note: You cannot assign value to a Parameter dynamically using some computation logic. All the values for defined parameters should be given as Input.
I like Fn::Sub, which is much cleaner and easy to read.
!Sub "/database/${Environment}/url"
I am stuck into the same problem, below are my findings:
We can compose values and descriptions while writing into the SSM Parameter Store from CloudFormation like below :
LambdaARN:
Type: AWS::SSM::Parameter
Properties:
Type: String
Description: !Sub "Lambda ARN from ${AWS::StackName}"
Name: !Sub "/secure/${InstallationId}/${AWS::StackName}/lambda-function-arn"
Value: !GetAtt LambdaFunction.Arn
We can not compose values/defaults to looking for in SSM Parameter Store. Like below:
Parameters:
...
LambdaARN:
Type: Type: AWS::SSM::Parameter::Value<String>
Value: !Sub "/secure/${InstallationId}/teststack/lambda-function-arn"
This is not allowed as per AWS Documentation[1]. Both(Sub/Join) Functions won't work. Following is the error which I was getting:
An error occurred (ValidationError) when calling the CreateChangeSet
operation: Template format error: Every Default member must be a
string.
Composing and passing values while creating stacks can be done like this:
Parameters:
...
LambdaARN:
Type: Type: AWS::SSM::Parameter::Value<String>
....
$ aws cloudformation deploy --template-file cfn.yml --stack-name mystack --parameter-overrides 'LambdaARN=/secure/devtest/teststack/lambda_function-arn'
If you add your custom tags while putting the values in the Parameter Store, it will overwrite the default tags added by CFN.
Default Tags:
- aws:cloudformation:stack-id
- aws:cloudformation:stack-name
- aws:cloudformation:logical-id
Every time we update the values in parameter store, it creates a new version, which is beneficial when we are using DynamicResolvers, this can serve as a solution to the problem in the question like
{{ resolve:ssm:/my/value:1}}
The last field is the version. Where different versions can point to different environments.
We are using versions with the parameters, and adding the labels to them[2], this can't be done via CFN[3], only possible way via CLI or AWS Console. This is AWS's way of handling multiple environments.
[1] https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
[2] https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-labels.html
[3] https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html