AWS CloudFormation function call fails: Fn::ImportValue must not depend on any resources, imported values, or Fn::GetAZs - aws-cloudformation

I have a cloud formation template (mainVPC) that creates few Subnets in a VPC and exports the subnets with names "PrivateSubnetA", "PrivateSubnetB" ...
I have a different cloud formation template that creates DBSubnetGroup. I want to use "PrivateSubnetA", "PrivateSubnetB" as default values if user does not provide data. CloundFormation does not support imported values in parameters. So I put some default value (XXXX) and had a condition section to see if the user has provided some input
Conditions:
userNotProvidedSubnetA: !Equals
- !Ref PrivateSubnetA
- XXXX
userNotProvidedSubnetB: !Equals
- !Ref PrivateSubnetB
- XXXX
This helps me in figuring out if the user has provided data. Now I want to use default values, if the user has not provided values, else use user-provided values.
below is code for that
DBSubnetGroup:
Type: 'AWS::RDS::DBSubnetGroup'
Properties:
DBSubnetGroupDescription: RDS Aurora Cluster Subnet Group
SubnetIds:
- !If
- userNotProvidedSubnetA
- Fn::ImportValue:
!Sub '${fmMainVpc}-PrivateSubnetA'
- !Ref PrivateSubnetA
- !If
- userNotProvidedSubnetB
- Fn::ImportValue:
!Sub '${fmMainVpc}-PrivateSubnetB'
- !Ref PrivateSubnetB
This fails with the error "Template error: the attribute in Fn::ImportValue must not depend on any resources, imported values, or Fn::GetAZs".
ImportValue is not used anywhere else in the template.
Is there a way for using exported values as default values ( the default values cannot be hardcoded, they come as exported values from a run of another stack), while providing an option for the users to provide their own values (to create resources).
Thanks.

This can also be caused by having a reference inside Fn::ImportValue to a parameter be misnamed. For example, if I have the following parameter NetworkStackName defined and I mis-reference it in the Fn::ImportValue statement (as NetworkName), I will get this error. I would need to change the NetworkName to match the value in Parameters, NetworkStackName to fix the error.
Parameters:
NetworkStackName:
Type: String
Default: happy-network-topology
Resources:
MySQLDatabase:
Type: AWS::RDS::DBInstance
Properties:
Engine: MySQL
DBSubnetGroupName:
Fn::ImportValue:
!Sub "${NetworkName}-DBSubnetGroup"

I had a problem where I needed to get my artifact bucket name from my prerequisite stack, I tried this:
Fn::ImportValue:
- 'arn:aws:s3:::${ArtifactStore}/*'
turns out you can do this and it will work. Hope his helps someone out one day!
- !Sub
- 'arn:aws:s3:::${BucketName}/*'
- BucketName : !ImportValue 'ArtifactStore'

Currently, Cloudformation didn't support dynamic default value. It's not possible to have a dynamic default value for CloudFormation. As the template has not executed at the time all parameters are being collected. However, you can use SSM parameter for as the workaround, something like below.
Parameters
PagerDutyUrl:
Type: AWS::SSM::Parameter::Value<String>
Description: The Pagerduty url
Going back to your current cloudformation, I am thinking that value ${fmMainVpc} might not be initialized correctly.

I'm my case, I had the follow resource:
# removed for brevity
Subnets:
- !ImportValue: parent-stack-subnet-a
- !ImportValue: parent-stack-subnet-b
I forgot to remove the : when changing the syntax from Fn::ImportValue to the shorthand !ImportValue. Confusing error message, but removing the : resolved it because that was incorrect usage on my part.

Related

Cloudformation conditional nested stack Unresolved resource dependencies

I have a Cloudformation stack that conditionally invokes a nested stack to create a RDS instance, only if an existing database URL is not passed in as a parameter.
If I pass a value to the DBExistingEndpoint parameter in the stack, the condition CreateDB is set to false, and it will not invoke the nested RDS stack at all.
The issue is that in the AutoScaling launch config resource, there is a conditional dependency. I need to reference either the URL output from the nested stack, or the URL passed in as a parameter to place in a file in the newly launched instance.
Parameters:
DBExistingEndpoint:
Type: String
Description: Set to a URL of a RDS instance to use an existing DB, otherwise create one
Default: ''
...
Conditions:
CreateDB:
!Equals [!Ref DBExistingEndpoint, '']
...
Resources:
# Database created only if existing URL not passed in
DB:
Type: AWS::CloudFormation::Stack
Condition: CreateDB
Properties:
TemplateURL: ...
...
ClusterInstanceLaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Metadata:
AWS::CloudFormation::Init:
config:
files:
/etc/dbenv:
mode: "000640"
owner: root
group: root
content:
!Join
- "\n"
-
- !Sub ["DB_HOST=${DBEndpointAddress}", DBEndpointAddress: !If [CreateDB, !GetAtt DB.Outputs.RDSEndPointAddress, !Ref DBExistingEndpoint]]
...
The issue is that if I pass in an existing endpoint URL, the DB resource is skipped (correctly), but the stack creation fails with Template format error: Unresolved resource dependencies [DB] in the Resources block of the template
Ideally the DB.output.RDSEndpointAddress reference in the ClusterInstanceLauchConfig resource should be ignored because the CreateDB condition in the !If is false
Does anybody know how to code around this limitation?
You should try to set the conditional statement on a different level than it is now.
What will work for sure, is having the conditional statement on the level of the LaunchConfiguration itself, which would also mean quite a lot of duplication of the code. But maybe you could try to see the conditional on the level of content or files etc, to see if there's a middle ground somewhere, to keep duplication low, but avoid the error you're getting right now.

Exclude/Include resource via parameter in AWS CloudFormation

I have a template that includes 3 resources. Is there a way to programmatically exclude 1 of the 3 resources by using a Parameter of my template?
(that is having the same result that i would get by commenting out the unwanted resource in my template)
It depends. Since you haven't specified any template, I can only show what I usually do.
Parameters:
SubnetId:
Type: String
Default: ''
Conditions:
HaveSubnetId:
!Not [!Equals [!Ref SubnetId, '']]
Resources:
MyInstance:
Condition: HaveSubnetId
Type: AWS::EC2::Instance
In this example, MyInstance is going to be created if SubnetId is given (i.e., not empty). If SubnetId is provided, HaveSubnetId will be true.
This is based on Condition section in a resource declaration.

Specify HostedZone NameServers as CloudFormation Outputs

I am creating a CFN stack for a number of domains. The domain are not with the AWS registry, but a third-party one.
I want to have the list of nameservers from the SOA as part of the stack Outputs. However, as they aren't returned as a string but, according to the docs, a "set", I can't figure out how to extract and return them.
Details:
According to the docs for AWS::Route53::HostedZone, you can obtain the list of nameservers with
Return Values
[...]
Fn::GetAtt
Fn::GetAtt returns a value for a specified attribute of this type. The
following are the available attributes and sample return values.
NameServers
Returns the **set** of name servers for the specific hosted zone. For example: ns1.example.com.
This attribute is not supported for private hosted zones.
So, I tried to do:
Resources:
MyZone:
Type: 'AWS::Route53::HostedZone'
Properties:
Name: my.domain.
...
Outputs:
MyZone:
Value: !Ref MyZone
MyZoneServers:
Value: !GetAtt MyZone.NameServers
but that gives:
An error occurred (ValidationError) when calling the UpdateStack operation: Template format error: The Value field of every Outputs member must evaluate to a String.
When I only output the zone ref, it works just fine and get the Z... string for the zone.
I've tried various other tricks and approaches, mostly with various intrinsic functions such as !Split, !Select, etc. Nowhere can I seem to find what this "set" is: a list? a comma-separated string? (in which case !Split should work)
I could retrieve the nameservers via the describe function of Route53 after the stack is created, but my feeling is that I'm missing something totally obvious so don't want to add that extra step.
The set of nameservers is an array of strings. In order to output it you need to use !Join like this:
Resources:
MyZone:
Type: 'AWS::Route53::HostedZone'
Properties:
Name: my.domain.
...
Outputs:
MyZone:
Value: !Ref MyZone
MyZoneServers:
Value: !Join [',', !GetAtt MyZone.NameServers] # or any other delimiter that suits you
You should see the following Outputs:

IAM nested stack fails to complete due to undefined resource policies

I have created a nested IAM stack, which constists of 3 templates:
- iam-policies
- iam-roles
-iam user/groups
the masterstack template looks like this:
Resources:
Policies:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/xxx/iam/iam_policies.yaml
UserGroups:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/xxx/iam/iam_user_groups.yaml
Roles:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/xxx/iam/iam_roles.yaml
The policy ARNs are exported via Outputs section like:
Outputs:
StackName:
Description: Name of the Stack
Value: !Ref AWS::StackName
CodeBuildServiceRolePolicy:
Description: ARN of the managed policy
Value: !Ref CodeBuildServiceRolePolicy
in the Role template the policies ARNs are imported like
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${EnvironmentName}-CodeBuildRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Path: /
ManagedPolicyArns:
- !GetAtt
- Policies
- Outputs.CodeBuildServiceRolePolicy
But when I try create the stack, it fails saying the Roles stack cannot be created because
Template error: instance of Fn::GetAtt references undefined resource Policies
How can I force the creation of the policies first so the second and third template can use the policies to create roles and user/ groups? Or is the issue elsewhere?
merci A
Your question,
How can I force the creation of the policies first so the second and
third template can use the policies to create roles and user/ groups?
Or is the issue elsewhere?
You can use "DependsOn" attribute. It automatically determines which resources in a template can be parallelized and which have dependencies that require other operations to finish first. You can use DependsOn to explicitly specify dependencies, which overrides the default parallelism and directs CloudFormation to operate on those resources in a specified order.
In your case second and third template DependsOn Policies
More details : DependsOn
The reason on why you aren't able to access the outputs is that, you haven't exposed the outputs for other stacks.
Update your Outputs with the data you want to export. Ref - Outputs for the same.
Then, use the function Fn::ImportValue in the dependent stacks to consume the required data. Ref - ImportValue for the same.
Hope this helps.

Add AWS::Route53::RecordSet DnsRecord to a serverless Cloudfront Distribution

I found this on how to associate a route53 dns record with a S3 bucket in a serverless.yml file.
I've tried to adapt that to the case of deploying a cloudfront distrib
DnsRecord:
Type: "AWS::Route53::RecordSet"
Properties:
AliasTarget:
DNSName: <cloudfrontdistribution id>
HostedZoneId: Z21DNDUVLTQW6Q
HostedZoneName: ${self:custom.appFQDN}.
Name:
Ref: WebAppCloudFrontDistribution
Type: 'CNAME'
but am struggling with how to get the distribution id as a ref rather than a fixed string.
How would I do this?
To set up an AliasTarget, you actually just need to provide the CloudFront DNS name for the DNSName parameter, not the distribution ID. You can do this with:
!GetAtt WebAppCloudFrontDistribution.DomainName
I'm assuming that WebAppCloudFrontDistribution is the logical ID of an AWS::CloudFront::Distribution resource in your template and not a parameter. If this is actually a parameter, just set the value of the parameter to the DNS name listed for the distribution in the AWS console dashboard for CloudFront.
There are some other things you'll need to fix in your template:
HostedZoneName should be the name of the Route53 hosted zone, not the FQDN you want to use. Personally, I prefer to use the HostedZoneId property for AWS::Route53::RecordSet resources instead since it's clearer what the meaning of this property is, but to each their own. (Note: HostedZoneId property for the AWS::Route53::RecordSet resource should be the HostedZoneId for YOUR hosted zone, not the same value as the AliasTarget HostedZoneId.)
Name should be the DNS name that you want to be a CNAME for the CloudFront distribution resource.
I know it's a bit weird, but with alias targets, you have to set the type to either "A" (for IPv4) or "AAAA" (IPv6). I recommend doing both - you can do this by creating a duplicate of your AWS::Route53::RecordSet resource but set type to "AAAA" instead of "A".
Finally, note that in order for this to work, you will also need to make sure to add the FQDN as an alternate name for the CloudFront distribution resource - you can set this using the "Aliases" property of the "DistributionConfig" property of the distribution resource in your template, or by configuring this manually for the distribution settings in the AWS console if you're not creating the resource in this template.
I struggled to create a AWS::Route53::RecordSet with CloudFormation producing unspecific, unhelpful error messages of the type "The resource failed to create". The key for me was to use HostedZoneId rather than HostedZoneName to specify the parent "hosted zone". This is what I ended up with:
NaaaaaComDNSEntry:
Type: 'AWS::Route53::RecordSet'
DependsOn: NaaaaaComCloudFront
Properties:
AliasTarget:
DNSName: !GetAtt NaaaaaComCloudFront.DomainName
# For CloudFront, HostedZoneId is always Z2FDTNDATAQYW2, see:
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid
HostedZoneId: Z2FDTNDATAQYW2
# HostedZoneId is for ID for 'naaaaa.com.'; In theory its valid to use `HostedZoneName` OR `HostedZoneId`
# but in practice the recordset always failed to create if I used `HostedZoneName`
HostedZoneId: ZABCDEFGHIJK5M
Name: 'www.naaaaa.com.'
Type: 'A'
This is what my working config looks like in serverless templates:
DnsRecord:
Type: "AWS::Route53::RecordSet"
Properties:
AliasTarget:
DNSName:
Fn::GetAtt:
- CloudFrontDistribution
- DomainName
# Looks like it is always the same for CloudFront distribs.
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html
# https://docs.aws.amazon.com/general/latest/gr/rande.html#cf_region
HostedZoneId: ${self:custom.zoneId}
HostedZoneName: ${self:custom.secondLevelDomain}.
Name: ${self:custom.appFQDN}
Type: 'A'
And
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
...
Aliases:
- ${self:custom.appFQDN}
Also courtesy of an example by Tom McLaughlin:
https://github.com/ServerlessOpsIO/serverless-zombo.com/blob/master/serverless.yml