7 Awesome CloudFormation Hacks
by Philipp Garbe
Since AWS introduced native YAML support, CloudFormation templates are much more readable than before. Also, the introduced intrinsic functions help a lot to build awesome templates. But sometimes the solution to a problem is not that obvious and often not well documented. This blog post should remind me to some of my hacks, so that I can google them later on. Maybe it’s helpful for you as well.
Hack I: Combine two sequent intrinsic functions
Problem: Due a restriction in YAML, it’s not possible to use the shortcut syntax for two sequent intrinsic functions. This is useful when a value should be imported and the variable name should be subsituted with the stack name.
Parameters:
ClusterStack:
Type: String
Resources:
Service:
Type: AWS::ECS::Service
Properties:
Cluster: !ImportValue !Sub '${ClusterStack}-ClusterName'
Solution: The solution is to use a combination of the standard and the tag syntax. The standard syntax has to be used as first and needs to be written in a new line.
Parameters:
ClusterStack:
Type: String
Resources:
Service:
Type: AWS::ECS::Service
Properties:
Cluster:
Fn::ImportValue: !Sub '${ClusterStack}-ClusterName'
Hack II: Use exported values from other stacks in !Sub
Problem:
The !Sub
function allows to replace a variable inside a string with the value of a stack parameter. But how can exports from other stacks be used?
Solution:
Not only a CloudFormation parameter can be used in a !Sub
function but also a custom parameter, which has to be defined as second argument to the !Sub
function. The value can be hardcoded or another intrinsic function like !ImportValue
.
Pipeline:
Type: AWS::CodePipeline::Pipeline
DependsOn: CloudFormationExecutionRole
Properties:
Stages:
- Name: DeployPipeline
Actions:
- Name: DeployPipelineAction
Configuration:
TemplatePath: 'Source::templates/pipeline.yaml'
ParameterOverrides: !Sub
- |
{
"VpcId": "${VpcId}"
}
- VpcId:
'Fn::ImportValue': !Sub '${VpcStack}-VpcId'
Also multiple parameters can be defined:
Pipeline:
Type: AWS::CodePipeline::Pipeline
DependsOn: CloudFormationExecutionRole
Properties:
Stages:
- Name: DeployPipeline
Actions:
- Name: DeployPipelineAction
Configuration:
TemplatePath: 'Source::templates/pipeline.yaml'
ParameterOverrides: !Sub
- |
{
"VpcId": "${VpcId}",
"Subnets": "${PrivateSubnets}"
}
- |
{
PrivateSubnets: !ImportValue vpc-stack-PrivateSubnetIds,
VpcId: !ImportValue vpc-stack-id
}
Hack III: Changes in cfn-init don’t trigger redeployment in AutoScaling Group
Problem:
I often use the cfn-init helper function instead of scripting all the things in UserData. Therefore my UserData is very small and normally doesn’t change. Unfortunately, changes at the cfn-init configuration are not detected by the AutoScaling Group and does not trigger a replacement of the existing ec2 instances. In worst case, the CloudFormation update is successful but due a bug in the cfn-init script the next ec2 instance that gets started uses the new launch configuration and fails.
Solution:
To detect bugs in cfn-init during the deployment, the UserData script needs to be changed. The easiest way is to add the current build number. With every build the UserData gets changed and forces the AutoScaling Group to replace all ec2 instances with the newer launch configuration.
Parameter:
BuildNumber:
Type: String
Resources:
LaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Metadata:
AWS::CloudFormation::Init:
configSets:
...
Properties:
UserData:
"Fn::Base64": !Sub |
#!/bin/bash
# This is needed for cfn-init to reinitialize the instances with the new version on updates
BUILD_NUMBER="${BuildNumber}"
/opt/aws/bin/cfn-init -v \
--stack ${AWS::StackName} \
--resource LaunchConfig \
--region ${AWS::Region}
/opt/aws/bin/cfn-signal -e $? \
--stack ${AWS::StackName} \
--region ${AWS::Region} \
--resource AutoScalingGroup
Hack IV: Get Stack name of sibling stack in nested stacks
Problem:
The problem happens when you create nested stacks and one stack needs the stack name of a sibling stack as parameter. Because the name of the stack is generated you don’t know that in advance. Unfortunately, !Ref
returns only the complete ARN (like arn:aws:cloudformation:us-east-1:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786) and not the stack name (like mystack-mynestedstack-sggfrhxhum7w)
You can find many production-ready cloudformation templates at https://github.com/widdix/aws-cf-templates
Solution: If you are in control of the stack templates you can return the actual StackName as output parameter.
# VPC-Stack:
Outputs:
StackName:
Value: !Ref AWS::StackName
Inside your parent stack you can now reference that output parameter:
# Parent Stack
Vpc:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/widdix-aws-cf-templates/vpc/vpc-3azs.yaml
Bastion:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/widdix-aws-cf-templates/vpc/vpc-ssh-bastion.yaml
Parameters:
ParentVPCStack: !GetAtt Vpc.Outputs.StackName
If you can’t change the template, the stack name can be extracted from ARN with !Select
and !Split
.
Vpc:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/widdix-aws-cf-templates/vpc/vpc-3azs.yaml
Bastion:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/widdix-aws-cf-templates/vpc/vpc-ssh-bastion.yaml
Parameters:
ParentVPCStack: !Select [1, !Split ['/', !Ref Vpc]] # Workaround to get the stack name
Hack V: AccountIds with leading zero
Problem: If you want to create a resource based policy like Lambda Permission the following error can bother you: The provided principal was invalid. Please check the principal and try again.
Solution: When you specifiy only the AccountId it’s treated as an integer internally at AWS. Therefore the leading zero gets truncated. As Workaround a full ARN has to be provided.
# AccountIds with leading zero need a special handling
LambdaInvokePermissionWithLeadingZero:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName: !Ref TriggerLambdaArn
Action: "lambda:InvokeFunction"
Principal: arn:aws:iam::012345678912:root
# AccountIds without leading zero can be used directly
LambdaInvokePermissionWithoutLeadingZero:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName: !Ref TriggerLambdaArn
Action: "lambda:InvokeFunction"
Principal: 123456789123
Hack VI: Use Dictionaries as Stack Parameter
Problem:
Unfortunately, there is no support to define the type of CloudFormation parameters as key-value pairs or dictionaries. This could be useful for properties like tags
or environment
.
Solution:
!If
functions can be used to return not only a single value but a whole block. The idea is to create an optional stack parameter and a condition for each key-value pair. That implies that the length of the list needs to be restricted.
The parameters can be used in a nested !If
construct which I can’t really describe, so have a look at the code (and don’t mess up the indention!).
Parameters:
Env1:
Type: String
Description: An item of possible environment variables
Default: ''
Env2:
Type: String
Description: An item of possible environment variables
Default: ''
Env3:
Type: String
Description: An item of possible environment variables
Default: ''
Conditions:
Env1Exist: !Not [ !Equals [!Ref Env1, '']]
Env2Exist: !Not [ !Equals [!Ref Env2, '']]
Env3Exist: !Not [ !Equals [!Ref Env3, '']]
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: !Ref AWS::StackName
Image: !Ref ImageName
Environment:
'Fn::If':
- Env1Exist
-
- Name: !Select [0, !Split ["|", !Ref Env1]]
Value: !Select [1, !Split ["|", !Ref Env1]]
- 'Fn::If':
- Env2Exist
-
Name: !Select [0, !Split ["|", !Ref Env2]]
Value: !Select [1, !Split ["|", !Ref Env2]]
- !Ref "AWS::NoValue"
- 'Fn::If':
- Env3Exist
-
Name: !Select [0, !Split ["|", !Ref Env3]]
Value: !Select [1, !Split ["|", !Ref Env3]]
- !Ref "AWS::NoValue"
- !Ref "AWS::NoValue"
Hack VII: DependsOn with condition
Problem:
It’s not possible to add a !If
function to the DependsOn attribute. This could be useful if the dependent resource itself has a Condition
.
In this example the ECS Service has a dependency to the ALBListenerRule. As the ALBListenerRule has a Condition, also the DependsOn has to have that condition which is Unfortunately not supported.
Application:
Type: AWS::ECS::Service
DependsOn: !If [HasAlb, WaitCondition, !Ref AWS::NoValue] # Not supported!
ALBListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: HasAlb
Properties:
...
Solution:
The dependency between ECS Service and the ALBListenerRule can be detoured to a WaitCondition. The WaitCondition can now implement the HasALB
condition at the Handle attribute and use either AlbWaitHandle
which depends on the ALBListenerRule or the WaitHandle
which has no further dependencies.
Parameters:
ContainerPort:
Type: String
Default: ''
Description: HTTP Port of the container
Conditions:
HasAlb: !Not [ !Equals [ !Ref ContainerPort, ""]]
Resources:
AlbWaitHandle:
Condition: HasAlb
DependsOn: ALBListenerRule
Type: "AWS::CloudFormation::WaitConditionHandle"
WaitHandle:
Type: "AWS::CloudFormation::WaitConditionHandle"
WaitCondition:
Type: "AWS::CloudFormation::WaitCondition"
Properties:
Handle: !If [HasAlb, !Ref AlbWaitHandle, !Ref WaitHandle]
Timeout: "1"
Count: 0
Application:
Type: AWS::ECS::Service
DependsOn: WaitCondition
ALBListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: HasAlb
Properties:
...
Subscribe via RSS