3 min.

How to dynamically transform AWS SAM parameters into a new variable for reuse

Learn how to modify AWS SAM parameters after declaring them in the Parameters section in order to create new values. This guide explains two methods for altering declared parameters and consolidating them into one variable for reuse.

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.