Preserve structure of YAML with ruamel yaml - ruamel.yaml

I've been using ruamel yaml to edit my YAML files and dump them back. I need help understanding how to keep the same structure as the original file has, because all I do is duplicate it, edit, and write it again.
For example, this is the original file:
ElasticLoadBalancingV2Listener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancer
Port: !FindInMap [NLBPorts, Port1, Port]
Protocol: "TCP"
DefaultActions:
-
Order: 1
TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroup1
Type: "forward"
The new file doesn't look the same:
ElasticLoadBalancingV2Listener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancer
Port: !FindInMap [NLBPorts, Port1, Port]
Protocol: "TCP"
DefaultActions:
- Order: 1
TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroup1
Type: "forward"
The biggest issue is that I used all sorts of tricks that ruamel has to fix this, but each time some different part of the yaml breaks.
This is my function:
def editEndpointServiceTemplate(endpoint_service_template_path):
yaml = YAML()
yaml.preserve_quotes = True
# yaml.compact(seq_seq=False, seq_map=False)
# yaml.indent(mapping=4, sequence=3, offset=0)
#Load yaml file
with open(endpoint_service_template_path) as fp:
data = yaml.load(fp)
#Edit the yaml
data['Description'] = "CloudFormation"
#Write new yaml file
with open(endpoint_service_template_path, 'w') as fp:
yaml.dump(data, fp)
As you can see with the commented commands, I tinkered around with the settings but couldn't find the sweet spot.

In this case it is obvious that your function did not produce the output you present
(different indent, missing word "CloudFormation"), but in general you should take care
in questions this is the same, and that your program is complete so that results can
more easily be reproduced.
ruamel.yaml does not have built in functions for all kinds of seldom seen format, but yours
is relatively close to the output using the method .indent(mapping=4, sequence=4, offset=2) and
transform by checking line by line.
As it is less likely that withing string scalars you have a sequence indicators followed by three spaces ("- ")
(which additionally would have to occur wrapped to be the first non-space on a line), you better do
.indent(mapping=4, sequence=4, offset=0) transform that:
import sys
import ruamel.yaml
yaml_str = """\
ElasticLoadBalancingV2Listener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancer
Port: !FindInMap [NLBPorts, Port1, Port]
Protocol: "TCP"
DefaultActions:
-
Order: 1
TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroup1
Type: "forward"
"""
yaml = ruamel.yaml.YAML()
yaml.indent(mapping=4, sequence=4, offset=0)
yaml.preserve_quotes = True
data = yaml.load(yaml_str)
data['Description'] = "CloudFormation"
def break_seq(s):
result = []
PAT = '- '
for line in s.splitlines():
ls_line = line.lstrip()
if ls_line.startswith(PAT):
line = line.replace(PAT, ' - \n' + ' ' * (line.index(PAT) + 4))
result.append(line)
return '\n'.join(result)
yaml.dump(data, sys.stdout, transform=break_seq)
which gives:
ElasticLoadBalancingV2Listener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancer
Port: !FindInMap [NLBPorts, Port1, Port]
Protocol: "TCP"
DefaultActions:
-
Order: 1
TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroup1
Type: "forward"
Description: CloudFormation
The above can be done by "hacking" the routines that serialize to sequences, but
it is often easier to just transform the output, although not as efficient in
time/space usage.
Unless the program consuming this output uses an incomplete YAML parser, the
actual data structure loaded will not change, it is just less readable for people
unused to such uncommon formatting.

Related

Creating two ALB with exception if value put in instance id then will create alb, but Not able to pass two instance id values in parameter section

Please help me with the I'm using to create two ALB with the exception if a value is put in one of the parameters then will create an alb that has the instances id but if I pass two instances id in the parameter ELB1InstanceIds then it gave me an error:
Properties validation failed for resource HTTPTG1 with message: #/Targets/0/Id: expected type: String, found: JSONArray
earlier I was using parameter `Type: 'ListAWS::EC2::Instance::Id' but when i don't select any instance in ELB2InstanceIds then I got an error that it required a selected value then only stack can be run.
so I have created with the type string and passed an instance id, it is working with one instance id but not working when I specify two instances id in one of the parameters.
Parameters:
ELB1InstanceIds:
Type: String
Default: i-12345678
ELB2InstanceIds:
Type: String
Default: i-12345678
Conditions:
IsELB1InstanceIdsNotEmpty: !Not [!Equals [!Ref ELB1InstanceIds, "i-12345678"]]
IsELB2InstanceIdsNotEmpty: !Not [!Equals [!Ref ELB2InstanceIds, "i-12345678"]]
Resources:
HTTPTG1:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
Targets:
- Id: !If
- IsELB1InstanceIdsNotEmpty
- - !Split [",", !Ref ELB1InstanceIds]
- !Ref 'AWS::NoValue'
HTTPTG2:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
Targets:
- Id: !If
- IsELB2InstanceIdsNotEmpty
- - !Split [",", !Ref ELB2InstanceIds]
- !Ref 'AWS::NoValue'
earlier I was using two parameters ELB1InstanceIds, and ELB2InstanceIds with Type: 'List<AWS::EC2::Instance::Id>' but when I don't select any instance in ELB2InstanceIds then I got an error that it required a selected value then only stack can be run.

CloudFormation TaskDefinition with two different entry points based on condition

I have to add taskdefintion entry based on condition. Below is the template i created.
Conditions
IsProdEnv: !Equals [ !Ref envtype, "prod" ]
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
EntryPoint:
- !If [IsProdEnv, !Split [",", ["python3", "hello.py"]], "./script.sh"]
I'm getting
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.
You have two strings instead of one: "python3" and "hello.py". You need to have one string, e.g. "python3, hello.py". How this string looks, depends on your use-case.

Cloudformation LaunchTemplate referencing IamInstanceProfile fails to create

I am trying to create a LaunchTemplate, which references an IamInstanceProfile, in my Cloudformation stack. Here is the code- i have omitted the irrelevant parts:
...
Resources:
ServerLaunchTemplate:
Type: 'AWS::EC2::LaunchTemplate'
Properties:
LaunchTemplateData:
InstanceType: !Ref InstanceType
SecurityGroups:
- !Ref SecGroup
IamInstanceProfile: !Ref ServerProfile
UserData:
...
ServerProfile:
Type: 'AWS::IAM::InstanceProfile'
Properties:
Path: /
Roles:
- !Ref ServerRole
...
The ServerProfile gets created successfully. However when the stack creation process reaches the step of creating the ServerLaunchTemplate, it fails with the error:
Property validation failure: [Value of property {/LaunchTemplateData/IamInstanceProfile} does not match type {Object}]
If i omit the reference to the IamInstanceProfile, the LaunchTemplate get created successfully.
According to the documentation and some examples this should work... Based on the error i understand, that the InstanceType field of the LaunchTemplate needs to reference an object, but "!Ref InstanceType" returns the resource id.
How can i fix this? How could i retrieve the object, that is presumably required by the "/LaunchTemplateData/IamInstanceProfile" field?
Thank you
Easy to miss in the docs: IamInstanceProfile requires an IamInstanceProfile Cloudformation object with the Arn of the referenced IamInstanceProfile being a property of it.
See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-iaminstanceprofile and https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-iaminstanceprofile.html.
This should work:
PortalLaunchTemplate:
Type: 'AWS::EC2::LaunchTemplate'
Properties:
LaunchTemplateName: !Sub ${InstanceName}-launch-template
LaunchTemplateData:
ImageId: !Ref AmiId
...
IamInstanceProfile:
Arn: !GetAtt InstanceProfile.Arn
What worked for me was:
"IamInstanceProfile":{ "Arn":
{
"Fn::Sub":"arn:aws:iam::${AWS::AccountId}:instanceprofile/${name_of_Instance_profile}"
}
Presumably because it requires the "Arn" value as a string.

YAML Error: could not determine a constructor for the tag

This is very similar to questions/44786412 but mine appears to be triggered by YAML safe_load(). I'm using Ruamel's library and YamlReader to glue a bunch of CloudFormation pieces together into a single, merged template. Is bang-notation just not proper YAML?
Outputs:
Vpc:
Value: !Ref vpc
Export:
Name: !Sub "${AWS::StackName}-Vpc"
No problem with these
Outputs:
Vpc:
Value:
Ref: vpc
Export:
Name:
Fn::Sub: "${AWS::StackName}-Vpc"
Resources:
vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
Fn::FindInMap: [ CidrBlock, !Ref "AWS::Region", Vpc ]
Part 2; how to get load() to leave what's right of the 'Fn::Select:' alone.
FromPort:
Fn::Select: [ 0, Fn::FindInMap: [ Service, https, Ports ] ]
gets converted to this, that now CF doesn't like.
FromPort:
Fn::Select: [0, {Fn::FindInMap: [Service, https, Ports]}]
If I unroll the statement fully then no problems. I guess the shorthand is just problematic.
FromPort:
Fn::Select:
- 0
- Fn::FindInMap: [Service, ssh, Ports]
Your "bang notation" is proper YAML, normally this is called a tag. If you want to use the safe_load() with those you'll have to provide constructors for the !Ref and !Sub tags, e.g. using:
ruamel.yaml.add_constructor(u'!Ref', your_ref_constructor, constructor=ruamel.yaml.SafeConstructor)
where for both tags you should expect to handle scalars a value. and not the more common mapping.
I recommend you use the RoundTripLoader instead of the SafeLoader, that will preserve order, comments, etc. as well. The RoundTripLoader is a subclass of the SafeLoader.
If you are using ruamel.yaml>=0.15.33, which supports round-tripping scalars, you can do (using the new ruamel.yaml API):
import sys
from ruamel.yaml import YAML
yaml = YAML()
yaml.preserve_quotes = True
data = yaml.load("""\
Outputs:
Vpc:
Value: !Ref: vpc # first tag
Export:
Name: !Sub "${AWS::StackName}-Vpc" # second tag
""")
yaml.dump(data, sys.stdout)
to get:
Outputs:
Vpc:
Value: !Ref: vpc # first tag
Export:
Name: !Sub "${AWS::StackName}-Vpc" # second tag
In older 0.15.X versions, you'll have to specify the classes for the scalar objects yourself. This is cumbersome, if you have many objects, but allows for additional functionality:
import sys
from ruamel.yaml import YAML
class Ref:
yaml_tag = u'!Ref:'
def __init__(self, value, style=None):
self.value = value
self.style = style
#classmethod
def to_yaml(cls, representer, node):
return representer.represent_scalar(cls.yaml_tag,
u'{.value}'.format(node), node.style)
#classmethod
def from_yaml(cls, constructor, node):
return cls(node.value, node.style)
def __iadd__(self, v):
self.value += str(v)
return self
class Sub:
yaml_tag = u'!Sub'
def __init__(self, value, style=None):
self.value = value
self.style = style
#classmethod
def to_yaml(cls, representer, node):
return representer.represent_scalar(cls.yaml_tag,
u'{.value}'.format(node), node.style)
#classmethod
def from_yaml(cls, constructor, node):
return cls(node.value, node.style)
yaml = YAML(typ='rt')
yaml.register_class(Ref)
yaml.register_class(Sub)
data = yaml.load("""\
Outputs:
Vpc:
Value: !Ref: vpc # first tag
Export:
Name: !Sub "${AWS::StackName}-Vpc" # second tag
""")
data['Outputs']['Vpc']['Value'] += '123'
yaml.dump(data, sys.stdout)
which gives:
Outputs:
Vpc:
Value: !Ref: vpc123 # first tag
Export:
Name: !Sub "${AWS::StackName}-Vpc" # second tag

!ImportValue in Serverless Framework not working

I'm attempting to export a DynamoDb StreamArn from a stack created in CloudFormation, then reference the export using !ImportValue in the serverless.yml.
But I'm getting this error message:
unknown tag !<!ImportValue> in "/codebuild/output/src/serverless.yml"
The cloudformation and serverless.yml are defined as below. Any help appreciated.
StackA.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Resources for the registration site
Resources:
ClientTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Retain
Properties:
TableName: client
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 2
WriteCapacityUnits: 2
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Outputs:
ClientTableStreamArn:
Description: The ARN for My ClientTable Stream
Value: !GetAtt ClientTable.StreamArn
Export:
Name: my-client-table-stream-arn
serverless.yml
service: my-service
frameworkVersion: ">=1.1.0 <2.0.0"
provider:
name: aws
runtime: nodejs6.10
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeStream
- dynamodb:GetRecords
- dynamodb:GetShardIterator
- dynamodb:ListStreams
- dynamodb:GetItem
- dynamodb:PutItem
Resource: arn:aws:dynamodb:*:*:table/client
functions:
foo:
handler: foo.main
events:
- stream:
type: dynamodb
arn: !ImportValue my-client-table-stream-arn
batchSize: 1
Solved by using ${cf:stackName.outputKey}
I struggled with this as well, and what did trick for me was:
functions:
foo:
handler: foo.main
events:
- stream:
type: dynamodb
arn:
!ImportValue my-client-table-stream-arn
batchSize: 1
Note, that intrinsic functions ImportValue is on a new line and indented, otherwise the whole event is ignored when cloudformation-template-update-stack.json is generated.
It appears that you're using the !ImportValue shorthand for CloudFormation YAML. My understanding is that when CloudFormation parses the YAML, and !ImportValue actually aliases Fn::ImportValue. According to the Serverless Function documentation, it appears that they should support the Fn::ImportValue form of imports.
Based on the documentation for Fn::ImportValue, you should be able to reference the your export like
- stream:
type: dynamodb
arn: {"Fn::ImportValue": "my-client-table-stream-arn"}
batchSize: 1
Hope that helps solve your issue.
I couldn't find it clearly documented anywhere but what seemed to resolve the issue for me is:
For the Variables which need to be exposed/exported in outputs, they must have an "Export" property with a "Name" sub-property:
In serverless.ts
resources: {
Resources: resources["Resources"],
Outputs: {
// For eventbus
EventBusName: {
Export: {
Name: "${self:service}-${self:provider.stage}-UNIQUE_EVENTBUS_NAME",
},
Value: {
Ref: "UNIQUE_EVENTBUS_NAME",
},
},
// For something like sqs, or anything else, would be the same
IDVerifyQueueARN: {
Export: {
Name: "${self:service}-${self:provider.stage}-UNIQUE_SQS_NAME",
},
Value: { "Fn::GetAtt": ["UNIQUE_SQS_NAME", "Arn"] },
}
},
}
Once this is deployed you can check if the exports are present by running in the terminal (using your associated aws credentials):
aws cloudformation list-exports
Then there should be a Name property in a list:
{
"ExportingStackId": "***",
"Name": "${self:service}-${self:provider.stage}-UNIQUE_EVENTBUS_NAME", <-- same as given above (but will be populated with your service and stage)
"Value": "***"
}
And then if the above is successful, you can reference it with "Fn::ImportValue" like so, e.g.:
"Resource": {
"Fn::ImportValue": "${self:service}-${self:provider.stage}-UNIQUE_EVENTBUS_NAME", <-- same as given above (but will be populated with your service and stage)
}