Dynamic parameters are not natively supported in AWS SAM. However, they can be achieved with some compromises. They are useful when you want to utilize already declared parameters in a different form. For instance, the following parameter transformation, which declares AccountBRoleARN
, is not possible with AWS SAM:
Parameters:
AccountB:
Type: String
AccountBRoleName:
Type: String
AccountBRoleARN:
Type: String
Default: !Sub "arn:aws:iam::${AccountB}:role/${AccountBRoleName}"
Even though intrinsic functions like !Sub
are possible in the Resources section, allowing for dynamic values, writing them can be prone to errors and difficult to read, especially with more complex functions or when you have to do this multiple times. The below blend of intrinsic functions gets the role name value from an ARN string, but it's generally not a good idea to write it more than once in the template for maintainability reasons:
!Select [0, !Split ["/", !Select [4, !Split [":", !Ref AccountBRoleARN]]]]
The ideal situation would be having the ability to declare such a transformation once and reuse it whenever required in the template. There are two ways to achieve this. While neither method is perfect, both will yield the desired results.
Declare Nested Template
One way to create flexible parameters that can be computed at build time and reused many times in the template is by declaring them in a nested template. Unfortunately, this solution has a significant drawback.
A nested template, similarly to any other template including the root template, needs to have a declared Resources section with at least one item. This requirement can be challenging, as there may not be a resource that you would like to declare in the nested template which i n fact should solely serve as a container for computed parameters.
In the root template, declare a nested template and pass parameters than need to be transformed:
\\ template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
AccountB:
Type: String
AccountBRoleName:
Type: String
AccountBRole2Name:
Resources:
Roles:
Type: AWS::Serverless::Application
Properties:
Location: nested_template.yaml
Parameters:
AccountB: !Ref AccountB
AccountBRoleName: !Ref AccountBRoleName
AccountBRoleName2: !Ref AccountBRoleName2
In the nested template, intercept passed parameters and output their modified form for further reuse:
\\ nested_template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
AccountB:
Type: String
AccountBRoleName:
Type: String
AccountBRole2Name:
Resources:
#declare some resource here
Outputs:
AccountBRoleArn:
Description: BuilderAccessCodePipelineRoleArn
Value: !Sub "arn:aws:iam::${AccountB}:role/${AccountBRoleName}"
AccountBRole2Arn:
Description: PublicWebsiteCodeDeployRoleArn
Value: !Sub "arn:aws:iam::${AccountB}:role/${AccountBRole2Name}"
Finally, use the computed parameters in the template when required, for example, in another nested template:
Website:
Type: AWS::Serverless::Application
Properties:
Location: website_template.yaml
Parameters:
AccountBRoleArn: !GetAtt Roles.Outputs.AccountBRoleArn
AccountBRole2Arn: !GetAtt Roles.Outputs.AccountBRole2Arn
Transform Parameters with Macros
The second solution relies on AWS SAM Custom Macros. Implementing a SAM Macro can be a bit bothersome because it must first be built and deployed to AWS before it can be used in other templates. Thus, there is no way to declare a macro within the same template as the target template where you intend to use the macro.
Personally, I consider the biggest drawback of using Macros to transform is that they won't work with local API and local invoke. The results will only be visible in the cloud.
Due to the lack of comprehensive documentation and tutorials on how to write and deploy custom macros using SAM, I will show you most of the code here, and the complete example can be found on the BlowStack GitHub.
Create SAM Macro
Macros work thanks to Lambdas which process the logic for them. To create a Macro, you need a Lambda first. Take a look at the following template which declares a Lambda with a reference to the path with code, and the Macro resource which references the Lambda.
\\sam-custom-macro-example\create-macro\template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-custom-macro-example - example of a declaring custom macro in SAM
Resources:
CustomMacroLambdaFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: lambdas/customMacro
Handler: app.convertToARN
Runtime: nodejs18.x
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: "es2020"
Sourcemap: true
EntryPoints:
- app.ts
Macro:
Type: AWS::CloudFormation::Macro
Properties:
Name: 'ArnGenerator'
Description: Combine account id and role name into full ARN
FunctionName: !GetAtt CustomMacroLambdaFunction.Arn
Nothing special happened so far. I chose a super easy example for our Lambda directly related to the previously mentioned roles examples. The Macro will receive 3 parameters: accountId, Name, and ArnType, then transform this into a specific ARN string. The first parameter will decide whether you want to transform into an IAM role or user, but the possibilities are endless.
\\sam-custom-macro-example\create-macro\lambdas\customMacro\app.ts
enum ArnTypes {
Role = 'Role',
USER = 'User'
}
export const convertToARN = async (event): Promise<any> => {
const response = {
requestId: event.requestId,
status: "success"
};
try {
const { AccountId, Name, ArnType } = event.params;
switch (ArnType) {
case ArnTypes.Role:
response.fragment = `arn:aws:iam::${AccountId}:role/${Name}`
break;
case ArnTypes.USER:
response.fragment = `arn:aws:iam::${AccountId}:user/${Name}`
break
default:
throw Error('invalid arn type provided')
}
} catch (error) {
console.log(error)
}
return response;
};
It is necessary to build and deploy the root template from the create-macro subfolder into AWS so you can reference this Lambda logic in the target template.
Apply SAM Macro
One way you can utilize a SAM Macro as a dynamic parameter is to insert its result into the Environment Variables section of a Lambda, as shown in the following template:"
\\sam-custom-macro-example\apply-macro\template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-custom-macro-example - example of a applying custom macro in SAM
Resources:
CustomFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: lambdas/customFunction
Handler: app.customFunction
Runtime: nodejs18.x
Environment:
Variables:
DynamicVariableFromMacro:
'Fn::Transform':
Name: 'ArnGenerator'
Parameters:
AccountId: 'AccountB'
Name: 'AccountBRoleName'
ArnType: 'Role'
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: "es2020"
Sourcemap: true
EntryPoints:
- app.ts
The lambda will only log the result in order to validate whether the environment was correctly processed and passed to the target Lambda.
\\sam-custom-macro-example\apply-macro\lambdas\customFunction\app.ts
export const customFunction = async (): Promise<{ body: string; statusCode: number }> => {
try {
return {
statusCode: 200,
body: JSON.stringify({
message: (process.env.DynamicVariableFromMacro),
}),
};
} catch (err) {
console.log(err);
return {
statusCode: 500,
body: JSON.stringify({
message: 'some error happened',
}),
};
}
};
Now, build the second template and deploy it. Invoke the customFunction in the AWS console to see the results.