What would be a good strategy to have a default value on mappings?
I.E.
I have a parameter called country
Based on that country I reference a DNS using mappings
"Mappings" : {
"DNS":{
"us" : {"dns" : "mypage.us.com", "ttl" : "600"},
"mx" : {"dns" : "mypage.default.com", "ttl" : "300"},
"ar" : {"dns" : "mypage.default.com", "ttl" : "300"},
"br" : {"dns" : "mypage.default.com", "ttl" : "300"}
}
}
If us it's been mapped:
{ "Fn::FindInMap" : [ "DNS", { "Ref" : "country" }, "dns" ]}
I get "mypage.us.com" for the other countries I've created a huge list of countries with a default value mypage.default.com, in the future, this values will be changing and we will be adding more countries, is there a better approach to this?
The only way I was able to do this was to chain Fn::If statements instead of using the map. I tried using a combination of Fn::If and Fn::FindInMap but Fn::FindInMap will always raise an error it if can't find the mapping.
Therefore the only solution for me was to resort to using something like the following (for me it was setting ecs memory based on instance type):
Conditions:
IsT2Micro: !Equals [!Ref InstanceType, "t2.micro"]
IsT2Small: !Equals [!Ref InstanceType, "t2.small"]
...
taskdefinition:
Type: AWS::ECS::TaskDefinition
Properties:
...
Memory: !If [ IsT2Micro, 900, !If [ IsT2Small, 1900, !Ref "AWS::NoValue"]]
To elaborate on Steve Smith's answer.
Cloudformation always expects a valid mapping, even behind a missed logic gate.
You can combine !Sub and !If for a fair amount of flexibility though.
For example, we do this for dynamic staging ECS Env vars:
Parameters:
Env:
Type: String
Branch:
Type: String
DevelopUrl:
Type: String
Default: "develop.example.com"
MasterUrl:
Type: String
Default: "master.example.com"
...(ECS Resource)
Environment:
- !If
- IsStaging
- Name: SOME_CALLBACK_URL
Value: !Sub
- "https://${Url}/some-callback-endpoint"
- Url: !If [ IsDevelop, !Ref DevelopUrl, !If [ IsMaster, !Ref MasterUrl, !GetAtt MyLoadBalancer.DNSName ] ]
- !Ref "AWS::NoValue"
Cloud Formation helps create AWS resources once in the beginning of their life. You can also do updates with it, but I think in your case it sounds like you'll be better off building your DNS config logic into your application. Maybe create a Database table in DynamoDB with the mapping data. You could pass the Country value into the servers as an environment variable, and have them query the DynamoDB table on launch based on their environment variable.
Alternatively, you can have Cloud Formation invoke a Lambda function when it launches a new stack to query DynamoDB to get the value of the DNS config based on the country so you don't have to keep modifying your stack JSON every time there's a new entry and don't have to change your application.
In your mapping, add a default entry:
"Mappings" : {
"DNS":{
"us" : {"dns" : "mypage.us.com", "ttl" : "600"},
"mx" : {"dns" : "mypage.mx.com", "ttl" : "300"},
"default" : {"dns" : "mypage.default.com", "ttl" : "300"}
}
}
Then create a condition (YAML):
Conditions:
HasSpecialDNS: !Or:
- !Equals [!Ref country, "us"]
- !Equals [!Ref country, "mx"]
Then change the 2nd parameter of FindInMap to:
{ "Fn::FindInMap" : [ "DNS", { "Fn::If": ["HasSpecialDNS", {"Ref" : "country"}, "default" ]}, "dns" ]}
Or YAML:
Fn::FindInMap:
- DNS
- !If ["HasSpecialDNS", !Ref country, "default" ]
- "dns"
Related
I want to create a EC2 instance type t3.medium on all environments and m5.large on production.
I'm using .ebextensions (YAML) like so:
option 1:
Mappings:
EnvironmentMap:
"production":
TheType: "m5.large"
SecurityGroup: "foo"
...
"staging":
TheType: "t3.medium"
SecurityGroup: "bar"
...
option_settings:
aws:autoscaling:launchconfiguration:
IamInstanceProfile: "aws-elasticbeanstalk-ec2-role"
InstanceType: !FindInMap
- EnvironmentMap
- !Ref 'AWSEBEnvironmentName'
- TheType
SecurityGroups:
- {"Fn::FindInMap": ["EnvironmentMap", {"Ref": "AWSEBEnvironmentName"}, "SecurityGroup"]}
Option 2:
InstanceType: {"Fn::FindInMap": ["EnvironmentMap", {"Ref": "AWSEBEnvironmentName"}, "EC2InstanceType"]}
Option 3:
InstanceType:
- {"Fn::FindInMap": ["EnvironmentMap", {"Ref": "AWSEBEnvironmentName"}, "EC2InstanceType"]}
Results
Option 1 fails with Invalid Yaml (but I took this from this AWS example.
Option 2 and 3 fail with the same problem.
The FindInMap function is not "called":
Invalid option value: '{"Fn::FindInMap":["EnvironmentMap","EC2InstanceType"]},{"Ref":"AWSEBEnvironmentName"}' (Namespace: 'aws:autoscaling:launchconfiguration', OptionName: 'InstanceType'): Value is not one of the allowed values: [c1.medium, c1.xlarge, c3.2xlarge, ....
It tries to interpret the whole function/thing as a string.
For the SecurityGroups property it works, for InstanceType it does not.
I can't do it dynamically and I can't find how to achieve this neither on AWS doc, SO, or anywhere else. I would assume this is simple stuff. What am I missing?
EDIT:
Option 4: using conditionals
Conditions:
IsProduction: !Equals [ !Ref AWSEBEnvironmentName, production ]
option_settings:
aws:autoscaling:launchconfiguration:
InstanceType: !If [ IsProduction, m5.large, t3.medium ]
SecurityGroups:
- {"Fn::FindInMap": ["EnvironmentMap", {"Ref": "AWSEBEnvironmentName"}, "SecurityGroup"]}
Error: YAML exception: Invalid Yaml: could not determine a constructor for the tag !Equals in...
But this comes from documentation on conditions and if.
EDIT 2:
I eventually found out that the option InstanceType is obsolute and we should use:
aws:ec2:instances
InstanceTypes: "t3.medium"
But alas, this does not solve the problem either because I cannot use the replacement functions here as well (Fn:findInMap).
The reason why FindInMap does not work in option_settings is the fact that only four intrinsic functions are allowed there (from docs):
Ref
Fn::GetAtt
Fn::Join
Fn::GetOptionSetting
I'm not convinced that SecurityGroups worked. I think your script failed before FindInMap in SecurityGroups got chance to be evaluated.
However, I tried to find a way using Resources. The closes I got was with the following config file:
Mappings:
EnvironmentMap:
production:
TheType: "t3.medium"
staging:
TheType: "t2.small"
Resources:
AWSEBAutoScalingLaunchConfiguration:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
InstanceType:
? "Fn::FindInMap"
:
- EnvironmentMap
-
Ref: "AWSEBEnvironmentName"
- TheType
Although this is a step closer, it ultimately fails as well. The reason is that when EB is jointing our Resources config file with its own template, it produces the following:
"InstanceType": {
"Ref": "InstanceType", # <--- this should NOT be here :-(
"Fn::FindInMap": [
"EnvironmentMap",
{
"Ref": "AWSEBEnvironmentName"
},
"TheType"
]
},
instead of
"InstanceType": {
"Fn::FindInMap": [
"EnvironmentMap",
{
"Ref": "AWSEBEnvironmentName"
},
"TheType"
]
},
And this happens because the original InstanceType (before the joint operation) is:
"InstanceType":{"Ref":"InstanceType"},
Therefore, EB instead of replacing InstanceType with our custom InstanceType provided in our config file, it just merges them.
I'm fighting with wired case.
I need to push cloudformation stacks dynamically parameterized with terraform.
My resource looks like this.
resource "aws_cloudformation_stack" "eks-single-az" {
count = length(var.single_az_node_groups)
name = "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}"
template_body = <<EOF
Description: "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}"
Resources:
ASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}"
VPCZoneIdentifier: ["${var.private_subnet_ids[count.index]}"]
MinSize: "${lookup(var.single_az_node_groups[count.index], "asg_min", "0")}"
MaxSize: "${lookup(var.single_az_node_groups[count.index], "asg_max", "10")}"
HealthCheckType: EC2
TargetGroupARNs: [] < - here is error.
MixedInstancesPolicy:
InstancesDistribution:
OnDemandBaseCapacity: "0"
OnDemandPercentageAboveBaseCapacity: "${lookup(var.single_az_node_groups[count.index], "on_demand_percentage", "0")}"
LaunchTemplate:
LaunchTemplateSpecification:
LaunchTemplateId: "${aws_launch_template.eks-single-az[count.index].id}"
Version: "${aws_launch_template.eks-single-az[count.index].latest_version}"
Overrides:
-
InstanceType: m5.large
Tags:
- Key: "Name"
Value: "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}"
PropagateAtLaunch: true
- Key: "kubernetes.io/cluster/${var.cluster_name}"
Value: "owned"
PropagateAtLaunch: true
- Key: "k8s.io/cluster-autoscaler/enabled"
Value: "true"
PropagateAtLaunch: true
- Key: "k8s.io/cluster-autoscaler/${var.cluster_name}"
Value: "true"
PropagateAtLaunch: true
UpdatePolicy:
AutoScalingRollingUpdate:
MinSuccessfulInstancesPercent: 80
MinInstancesInService: "${lookup(data.external.desired_capacity.result, "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}", "0")}"
PauseTime: PT4M
SuspendProcesses:
- HealthCheck
- ReplaceUnhealthy
- AZRebalance
- AlarmNotification
- ScheduledActions
WaitOnResourceSignals: true
EOF
depends_on = [
aws_launch_template.eks-single-az
]
}
I need to put target groups arn from list containing json objects:
single_az_node_groups = [
{
"name" : "workload-az1",
"instance_type" : "t2.micro",
"asg_min" : "1",
"asg_max" : "7",
"target_group_arns" : "arnA, arnB, arnC"
},
...
]
I tried everything. Problem is that i tried many terraform functions and all the time terraform is addding some double-quotes which cloudformation does not support or terraform won't process the template_body becuase of missing quotes..
Do you know meybe some sneaky trick how to achive that ?
When building strings that represent serialized data structures, it's much easier to use Terraform's built-in serialization functions to construct the result, rather than trying to produce a valid string using string templates.
In this case, we can use jsonencode to construct a JSON string representing the template_body from a Terraform object value, which then allows using all of the Terraform language expression features to build it:
template_body = jsonencode({
Description: "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}",
Resources: {
ASG: {
Type: "AWS::AutoScaling::AutoScalingGroup",
Properties: {
AutoScalingGroupName: "eks-${var.cluster_name}-${var.single_az_node_groups[count.index].name}",
VPCZoneIdentifier: [var.private_subnet_ids[count.index]],
MinSize: lookup(var.single_az_node_groups[count.index], "asg_min", "0"),
MaxSize: lookup(var.single_az_node_groups[count.index], "asg_max", "10"),
HealthCheckType: "EC2",
TargetGroupArns: flatten([
for g in local.single_az_node_groups : [
split(", ", g.target_group_arns)
]
]),
# etc, etc
},
},
},
})
As you can see above, by using jsonencode for the entire data structure we can then use Terraform expression operators to build the values. For TargetGroupArns in the above example I used the flatten function along with a for expression to transform the nested local.single_az_node_groups data structure into a flat list of target group ARN strings.
CloudFormation supports both JSON and YAML, and Terraform also has a yamlencode function that you could potentially use instead of jsonencode here. I chose jsonencode both because yamlencode is currently marked as experimental (the exact YAML formatting it produces may change in a later release) and because Terraform has special support for JSON formatting in the plan output where it can show a structural diff of the data structure inside, rather than a string-based diff.
I want to create Route53 HostedZone with CloudFormation so I want to check some information in Route53 about HostedZone is exist.
In logic of my case I need check if resource is exist, ignore the resource creation. How I can handle this problem.
My CloudFormation template show at below.
"myDNSRecord" : {
"Type" : "AWS::Route53::RecordSet",
"Properties" : {
"HostedZoneName" : { "Ref" : "HostedZoneResource" },
"Comment" : "DNS name for my instance.",
"Name" : {
"Fn::Join" : [ "", [
{"Ref" : "Ec2Instance"}, ".",
{"Ref" : "AWS::Region"}, ".",
{"Ref" : "HostedZone"} ,"."
] ]
},
"Type" : "A",
"TTL" : "900",
"ResourceRecords" : [
{ "Fn::GetAtt" : [ "Ec2Instance", "PublicIp" ] }
]
}
}
This is not exactly the answer you need. But in general, you can use Conditions for this. In you template, you define your condition in Conditions section and use it to conditionally create the resource. e.g.
Parameters:
EnvironmentSize:
Type: String
Default: Micro
AllowedValues:
- Micro
- Small
- Medium
- AuroraCluster
Conditions:
isntAuroraCluster:
!Not [!Equals [!Ref EnvironmentSize, "AuroraCluster"]]
DBInstance:
Type: AWS::RDS::DBInstance
Condition: isntAuroraCluster
Properties:
DBInstanceClass: !FindInMap [InstanceSize, !Ref EnvironmentSize, DB]
<Rest of properties>
Here my RDS DBinstance is only created if my environment size is not AuroraCluster.
If you don't find a better solution, you could take that as user input (whether to create a record set or not) & use that as condition to create your resource. Hope it helps.
The best way to do this would be to do the following:
Create a lambda backed custom resource
Check using lambda whether your resource exists or not, depending on that return an identifier
Use cloudformation conditions to check on the value of the returned identifier and then correspondingly create or not create the resource.
You can fetch the return value of the custom resource using !GetAtt
More information can be found on the AWS websites relating to custom resource:
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html
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]
I searching for what are the metadata that AWS uses for each and every service. For AWS instance it uses ami-id, ami_launch-index, hostname, instance-action and many more as the metadata like wise where i can find metadata for all the services like EBS, VPN....
Reference
Information obtained from Amazon Elastic Compute Cloud User Guide for Microsoft Windows Instances pdf pg no : 232 – 242
I believe you are interested in knowing the properties of the each AWS Resource / Service and not the meta-data. I don't think there is a straight answer. The work around what I can recommend is using the AWS CloudFormation's Syntax definition of each AWS Resource.
For Example :
EC2 Instance is represented by the following syntax. Not all of them are mandatory. http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html
Look at the KEYS of the Key:Value pair provided below [ "AvailabilityZone" : String ]
{
"Type" : "AWS::EC2::Instance",
"Properties" : {
"AvailabilityZone" : String,
"BlockDeviceMappings" : [ EC2 Block Device Mapping, ... ],
"DisableApiTermination" : Boolean,
"EbsOptimized" : Boolean,
"IamInstanceProfile" : String,
"ImageId" : String,
"InstanceInitiatedShutdownBehavior" : String,
"InstanceType" : String,
"KernelId" : String,
"KeyName" : String,
"Monitoring" : Boolean,
"NetworkInterfaces" : [ EC2 Network Interface, ... ],
"PlacementGroupName" : String,
"PrivateIpAddress" : String,
"RamdiskId" : String,
"SecurityGroupIds" : [ String, ... ],
"SecurityGroups" : [ String, ... ],
"SourceDestCheck" : Boolean,
"SsmAssociations" : [ SSMAssociation, ... ]
"SubnetId" : String,
"Tags" : [ Resource Tag, ... ],
"Tenancy" : String,
"UserData" : String,
"Volumes" : [ EC2 MountPoint, ... ],
"AdditionalInfo" : String
}
}
For VPC [http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html]
{
"Type" : "AWS::EC2::VPC",
"Properties" : {
"CidrBlock" : String,
"EnableDnsSupport" : Boolean,
"EnableDnsHostnames" : Boolean,
"InstanceTenancy" : String,
"Tags" : [ Resource Tag, ... ]
}
}
For EBS Volume [http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html]
{
"Type":"AWS::EC2::Volume",
"Properties" : {
"AutoEnableIO" : Boolean,
"AvailabilityZone" : String,
"Encrypted" : Boolean,
"Iops" : Number,
"KmsKeyId" : String,
"Size" : String,
"SnapshotId" : String,
"Tags" : [ Resource Tag, ... ],
"VolumeType" : String
}
}
The CloudFormation Resource Page has details for most of the items [http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html]. Below is the current list as of today [7 Jan 2016]
AWS::AutoScaling::AutoScalingGroup
AWS::AutoScaling::LaunchConfiguration
AWS::AutoScaling::LifecycleHook
AWS::AutoScaling::ScalingPolicy
AWS::AutoScaling::ScheduledAction
AWS::CloudFormation::Authentication
AWS::CloudFormation::CustomResource
AWS::CloudFormation::Init
AWS::CloudFormation::Interface
AWS::CloudFormation::Stack
AWS::CloudFormation::WaitCondition
AWS::CloudFormation::WaitConditionHandle
AWS::CloudFront::Distribution
AWS::CloudTrail::Trail
AWS::CloudWatch::Alarm
AWS::CodeDeploy::Application
AWS::CodeDeploy::DeploymentConfig
AWS::CodeDeploy::DeploymentGroup
AWS::CodePipeline::CustomActionType
AWS::CodePipeline::Pipeline
AWS::Config::ConfigRule
AWS::Config::ConfigurationRecorder
AWS::Config::DeliveryChannel
AWS::DataPipeline::Pipeline
AWS::DirectoryService::MicrosoftAD
AWS::DirectoryService::SimpleAD
AWS::DynamoDB::Table
AWS::EC2::CustomerGateway
AWS::EC2::DHCPOptions
AWS::EC2::EIP
AWS::EC2::EIPAssociation
AWS::EC2::Instance
AWS::EC2::InternetGateway
AWS::EC2::NetworkAcl
AWS::EC2::NetworkAclEntry
AWS::EC2::NetworkInterface
AWS::EC2::NetworkInterfaceAttachment
AWS::EC2::PlacementGroup
AWS::EC2::Route
AWS::EC2::RouteTable
AWS::EC2::SecurityGroup
AWS::EC2::SecurityGroupEgress
AWS::EC2::SecurityGroupIngress
AWS::EC2::SpotFleet
AWS::EC2::Subnet
AWS::EC2::SubnetNetworkAclAssociation
AWS::EC2::SubnetRouteTableAssociation
AWS::EC2::Volume
AWS::EC2::VolumeAttachment
AWS::EC2::VPC
AWS::EC2::VPCDHCPOptionsAssociation
AWS::EC2::VPCEndpoint
AWS::EC2::VPCGatewayAttachment
AWS::EC2::VPCPeeringConnection
AWS::EC2::VPNConnection
AWS::EC2::VPNConnectionRoute
AWS::EC2::VPNGateway
AWS::EC2::VPNGatewayRoutePropagation
AWS::ECS::Cluster
AWS::ECS::Service
AWS::ECS::TaskDefinition
AWS::EFS::FileSystem
AWS::EFS::MountTarget
AWS::ElastiCache::CacheCluster
AWS::ElastiCache::ParameterGroup
AWS::ElastiCache::ReplicationGroup
AWS::ElastiCache::SecurityGroup
AWS::ElastiCache::SecurityGroupIngress
AWS::ElastiCache::SubnetGroup
AWS::ElasticBeanstalk::Application
AWS::ElasticBeanstalk::ApplicationVersion
AWS::ElasticBeanstalk::ConfigurationTemplate
AWS::ElasticBeanstalk::Environment
AWS::ElasticLoadBalancing::LoadBalancer
AWS::IAM::AccessKey
AWS::IAM::Group
AWS::IAM::InstanceProfile
AWS::IAM::ManagedPolicy
AWS::IAM::Policy
AWS::IAM::Role
AWS::IAM::User
AWS::IAM::UserToGroupAddition
AWS::Kinesis::Stream
AWS::KMS::Key
AWS::Lambda::EventSourceMapping
AWS::Lambda::Function
AWS::Lambda::Permission
AWS::Logs::Destination
AWS::Logs::LogGroup
AWS::Logs::LogStream
AWS::Logs::MetricFilter
AWS::Logs::SubscriptionFilter
AWS::OpsWorks::App
AWS::OpsWorks::ElasticLoadBalancerAttachment
AWS::OpsWorks::Instance
AWS::OpsWorks::Layer
AWS::OpsWorks::Stack
AWS::RDS::DBCluster
AWS::RDS::DBClusterParameterGroup
AWS::RDS::DBInstance
AWS::RDS::DBParameterGroup
AWS::RDS::DBSecurityGroup
AWS::RDS::DBSecurityGroupIngress
AWS::RDS::DBSubnetGroup
AWS::RDS::EventSubscription
AWS::RDS::OptionGroup
AWS::Redshift::Cluster
AWS::Redshift::ClusterParameterGroup
AWS::Redshift::ClusterSecurityGroup
AWS::Redshift::ClusterSecurityGroupIngress
AWS::Redshift::ClusterSubnetGroup
AWS::Route53::HealthCheck
AWS::Route53::HostedZone
AWS::Route53::RecordSet
AWS::Route53::RecordSetGroup
AWS::S3::Bucket
AWS::S3::BucketPolicy
AWS::SDB::Domain
Try using AWS CLI.
You can execute the describe commands of the various services to see and understand the metadata
I am creating some DNS entries in my cloudformation. There is a param passed into the cfn script, which results in the creation of a Route53 entry like hostname-test.example.com:
"Host" : {
"Type" : "AWS::Route53::RecordSetGroup",
"Properties" : {
"HostedZoneName" : "example.com.",
"RecordSets" : [
{
"Name" : {
"Fn::Join" : [ "-", [
{"Ref" : "Hostname" },
"test.example.com"
]]
},
"Type" : "A",
"AliasTarget" : {
"DNSName" : { "Fn::GetAtt" : [ "PublicWebLoadBalancer", "CanonicalHostedZoneName" ] },
"HostedZoneId" : { "Fn::GetAtt" : [ "PublicWebLoadBalancer", "CanonicalHostedZoneNameID" ] }
}
}
]
}
}
In my output, I would like to get the Name attribute from the RecordSet, but I don't know how to reference it. According to the Fn::GetAtt documentation, Route53 objects are not supported.
Is this possible?
This question is a bit old, but it I just ran into this same issue.
You need to output the entire RecordSet, ie:
"Outputs" : {
"MyDNSRecord" : {
"Description": "The DNS Record of ...",
"Value" : { "Ref": "MyRecordSet" }
}
}
Which (not intuitively) outputs the value of the record set name you are looking for.
I had the same question, and was looking for a clear answer in yaml.
Given the following AWS::Route53::RecordSet
rPublicReverseProxyNLBDnsRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Ref pPublicHostedZoneName
Comment: !Sub 'DNS record for the ${AWS::StackName} ELB front door.'
Name: !Sub '${pDeploymentType}.${pPublicHostedZoneName}'
Type: CNAME
TTL: '30'
ResourceRecords:
- !GetAtt rPublicReverseProxyNLB.DNSName
I was able to output the Application URL I wanted with the following output section code:
Outputs:
ApplicationURL:
Description: 'The public URL for the application'
Value: !Sub 'https://${rPublicReverseProxyNLBDnsRecord}/'
Instead of embedding your RecordSet inside the RecordSetGroup, define it as a separate property, with the same HostedZoneName as your RecordSetGroup.
You can then use "Ref" to get the value of the Name attribute.
http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html
I second the suggestion of trying RecordSet.
But your "Name" is deterministic. If the stack completes, outputting what you already have will never behave differently from what you want:
{
"Fn::Join" : [ "-", [
{"Ref" : "Hostname" },
"test.example.com"
]]
}
If this were OOP, I'd say it's decidedly wrong to kick back an argument without taking the opportunity to implicitly test the function.