I'm very unclear on how references or variables work with CloudFormation.
Currently my iAmRole in my serverless.yml looks like:
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# Restrict our IAM role permissions to
# the specific table for the stage
Resource:
- "Fn::GetAtt": [ ReportsTable, Arn ]
ReportsTable is a table created in another file that looks like:
Resources:
ReportsTable:
Type: AWS::DynamoDB::Table
Properties:
...
LocalSecondaryIndexes:
- IndexName: typeId-accessToken-index
KeySchema:
- AttributeName: typeId
KeyType: HASH
...etc
I understand that the second value in the Fn::GetAtt array is referencing an attributename, but I don't understand where Arn is coming from. It seems like a variable but it's not defined anywhere.
Ultimately I need to add another Effect, Action, Resource block referencing the local secondary index I have created, but I'm lost as to where to start.
Edit: Looks like Arn comes from dynamoDB tables return values (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html)
Edit2: Okay so I now have the format arn:aws:dynamodb:region:account-id:table/table-name/index/* from the permissions reference docs, testing now.
You can use Cloudformation intrinsic function Sub to create index arn
!Sub '${ReportsTable.Arn}/index/*'
After referring to these docs: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html)
and these: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/api-permissions-reference.html
I was able to figure out that the required format for referencing an index table is arn:aws:dynamodb:region:account-id:table/table-name/index/*.
Further, in order to not hard code all the values in (in my case because I have several staging environments) you can do a join like so:
Fn::Join:
- ''
-
- 'arn:aws:dynamodb:'
- Ref: AWS::Region
- ':'
- Ref: AWS::AccountId
- ':table/'
- ${self:custom.tableName}/
- 'index/*'
Where table name is defined in your custom block.
Related
I would like to reference an Importvalue in the following Object, but I can't get it to work.
I want to put the value of the export name from another stack.
Input: !Sub "{\"InstanceId\":[\"{ Fn::ImportValue: !Sub ${ProjectName}-ec2-instance-id }\"],\"AutomationAssumeRole\":[\"${EC2AmazonSSMAutomationRole.Arn}\"]}"
This part of cloudformation template works as expected:
Parameters:
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: /aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-ebs
But when I changed it to something like this, I get an error:
Parameters:
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: /aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-ebs
AllowedValues:
- /aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-ebs
- ami-XXadfa6e17bbca4XX
I want to allow 2 values for LastestAmiId field. I will use this value in Properties:
MySpotFleet:
Type: 'AWS::EC2::SpotFleet'
Properties:
SpotFleetRequestConfigData:
LaunchSpecifications:
- ImageId: !Ref LatestAmiId
This works if I keep only 1 SSM Parameter value as shown above. But how do I allow my custom AMI along with default AMI?
Sadly you can't do this, since ami-XXadfa6e17bbca4XX is not the type of AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>.
You need to have two separate parameters, e.g. LatestAmiId and LatestAmiId2 with their respective types. Then use conditions in your template to choose between the one which is selected by the user.
In this learning exercise I want to use a PyPlate script to provision the BucketA, BucketB and BucketC buckets in addition to the TestBucket.
Imagine that the BucketNames parameter could be set by a user of this template who would specify a hundred bucket names using UUIDs for example.
AWSTemplateFormatVersion: "2010-09-09"
Transform: [PyPlate]
Description: A stack that provisions a bunch of s3 buckets based on param names
Parameters:
BucketNames:
Type: CommaDelimitedList
Description: All bucket names that should be created
Default: BucketA,BucketB,BucketC
Resources:
TestBucket:
Type: "AWS::S3::Bucket"
#!PyPlate
output = []
bucket_names = params['BucketNames']
for name in bucket_names:
output.append('"' + name + '": {"Type": "AWS::S3::Bucket"}')
The above when deployed responds with a Template format error: YAML not well-formed. (line 15, column 3)
Although the accepted answer is functionally correct, there is a better way to approach this.
Essentially PyPlate code recursively reads through all the key-value pairs of the stack and replaces the values with their Python computed values (ie they match the #!PyPlate regex). So we need to have a corresponding key to the PyPlate code.
Here's how a combination of PyPlate and Explode would solve the above problem.
AWSTemplateFormatVersion: "2010-09-09"
Transform: [PyPlate, Explode]
Description: A stack that provisions a bunch of s3 buckets based on param names
Parameters:
BucketNames:
Type: CommaDelimitedList
Description: All bucket names that should be created
Default: BucketA,BucketB,BucketC
Mappings: {}
Resources:
MacroWrapper:
ExplodeMap: |
#!PyPlate
param = "BucketNames"
mapNamespace = param + "Map"
template["Mappings"][mapNamespace] = {}
for bucket in params[param]:
template["Mappings"][mapNamespace][bucket] = {
"ResourceName": bucket
}
output = mapNamespace
Type: "AWS::S3::Bucket"
TestBucket:
Type: "AWS::S3::Bucket"
This approach is powerful because:
You can append resources to an existing template, because you won't tamper with the whole Resources block
You don't need to rely on hardcoded Mappings, as required by Explode. You can drive dynamic logic in Cloudformation.
Most of the Cloudformation props/KV's remain in the YAML part, and there is minimal python part which augments to the CFT functionality.
Please be aware of the macro order through - PyPlate needs to be executed before Explode, which is why the order [PyPlate, Explode]. The execution is sequential.
If we walk through the source code of PyPlate, it gives us control of more template related variables to work with, namely
params (stack parameters)
template (the entire template)
account_id
region
I utilised the template variable in this case.
Hope this helps
This works for me:
AWSTemplateFormatVersion: "2010-09-09"
Transform: [PyPlate]
Description: A stack that provisions a bunch of s3 buckets based on param names
Parameters:
BucketNames:
Type: CommaDelimitedList
Description: All bucket names that should be created
Default: BucketA,BucketB,BucketC
Resources:
|
#!PyPlate
output = {}
bucket_names = params['BucketNames']
for name in bucket_names:
output[name] = {"Type": "AWS::S3::Bucket"}
Explanation:
The python code outputs a dict object where the key is the bucket name and the value is its configuration:
{'BucketA': {'Type': 'AWS::S3::Bucket'}, 'BucketB': {'Type': 'AWS::S3::Bucket'}}
Prior to Macro execution, the YAML template is transformed to JSON format, and because the above is valid JSON data, I can plug it in as value of Resources.
(Note that having the hardcoded TestBucket won't work with this and I had to remove it)
Is there a way to create some kind of random or unique value in a CloudFormation template?
Why I need this. In our templates we have a number of custom-named resources, for instance AWS::AutoScaling::LaunchConfiguration with specified LaunchConfigurationName or AWS::AutoScaling::AutoScalingGroup with specified AutoScalingGroupName.
When updating stacks, we often get the following error:
CloudFormation cannot update a stack when a custom-named resource requires replacing. Rename some-stack-launch-configuration and update the stack again.
We don't want to rename resources just because we need to update them.
We also don't want to drop custom names in our resources. We won't mind however having some random suffix in our custom names.
With a "random generator" the solution might look something like:
MyAutoScalingGroup:
Type: 'AWS::AutoScaling::AutoScalingGroup'
Properties:
AutoScalingGroupName: !Sub 'my-auto-scaling-group-${AWS::Random}'
If you just need a random ID (no passwords, no fancy requirements), the way I'd recommend is using a portion of AWS::StackId, which is in the following format:
arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123
So in order to get the last portion, you would need two splits, e.g.:
AutoScalingGroupName:
Fn::Join:
- '-'
- - my-auto-scaling-group
- Fn::Select:
- 4
- Fn::Split:
- '-'
- Fn::Select:
- 2
- Fn::Split:
- /
- Ref: AWS::StackId
Equivalent shorter syntax:
AutoScalingGroupName: !Join ['-', ['my-auto-scaling-group', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]]
Meaning:
Start with AWS::StackId, e.g.: arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123
Split on / and select 2th portion (0-indexed): 51af3dc0-da77-11e4-872e-1234567db123
Split on - and select 4th portion (0-indexed): 1234567db123
Join with your fixed portion name: my-auto-scaling-group-1234567db123.
Advantages: I prefer this way than creating a CustomResource, because for large AWS environments and many stacks, you might end up with several lambdas, making governance a bit harder.
Disadvantages: It's more verbose (Fn::Join, Fn::Select, and Fn::Split).
EDIT 2022-02-17:
As observed by #teuber789's comment, if you need multiple resources of the same type, e.g.: my-auto-scaling-group-<random_1> and my-auto-scaling-group-<random_2>, this approach won't work as AWS::StackId is the same for whole stack.
this is similar to https://stackoverflow.com/a/67162053/2660313 but shorter:
Value: !Select [2, !Split ['/', !Ref AWS::StackId]]
In my opinion, the most elegant way to implement such logic (if you don't want to rename resources) is to use Cloudformation Macros. They're like a custom resource, but you call them implicitly during template transformation.
So, I will try to provide some example, but you can investigate more in AWS Documentation.
First of all, you create the function (with all required permissions and so on) that will do the magic (something like LiuChang mentioned).
Then, you should create a macro from this Function:
Resources:
Macro:
Type: AWS::CloudFormation::Macro
Properties:
Name: <MacroName>
Description: <Your description>
FunctionName: <Function ARN>
And then use this Macro in your resources definition:
MyAutoScalingGroup:
Type: 'AWS::AutoScaling::AutoScalingGroup'
Properties:
AutoScalingGroupName:
'Fn::Transform':
- Name: <MacroName>
Parameters:
InputString: <Input String>
...<Some other parameters like operation type or you can skip this>
Also, to use macros, you should specify the CAPABILITY_AUTO_EXPAND capability during stack creation/updation.
And that's it. It should just work, but of course one of the drawbacks of this approach - you should maintain additional lambda function.
I think you need to create a Lambda function to do this.
Here's a GitHub project cloudformation-random-string, which has a Lambda function and a simple tutorial.
Here's another tutorial Generate Passwords in AWS CloudFormation Template.
You can refer to the Lambda function above and make it work for you.
I'm using the AWS Java SDK to run some Stack Update command.
I generate random value using Java, then I pass it as parameter.
I'm trying to create a resource with one of its properties not being a constant value. Sounds like a job for a stack parameter, except that it's a string that can take form of a Ref function in some cases. Specifically, if it's the initial creation, I want the parameter value to be a Ref to another resource, and if it's a subsequent update, I want it to be a Ref to a stack parameter. Is this possible? Is there a function or a pseudo parameter, like AWS::CurrentAction that can take values like create and update, or anything of that kind?
I think it's something to be avoided but if you can't find any other alternative I have a workaround.
Here's an exemple with a bucket name:
Parameters:
ExternalBucketName:
Type: String
Default: ''
Conditions:
ExternalBucketNameSpecified:
!Not [!Equals [!Ref ExternalBucketName, '']]
Resources:
CFManagedBucket:
Type: AWS::S3::Bucket
SomeResource:
Type: AWS::Resource::XYZ
Properties:
BucketName: !If [ExternalBucketNameSpecified, !Ref ExternalBucketNameSpecified, !Ref CFManagedBucket]
When it's time to use the bucket created by this stack just set the ExternalBucketName empty and the stack will adapt automatically.