0

I have a CloudFormation template creating a Cloudwatch Synthetics Canary. Part of the template has a Lambda embedded written in Node.js 2: syn-nodejs-2.0. I have a few parameters that are being passed into the CFT and I want to pass them into the node script to use the values of the website I'm trying to test. I'm pretty sure I can do this with something like this:

{ "Ref" : "${Param}" } 

where ${Param} is the Cloudformation parameter I'm trying to reference, but that doesn't seem to work for me. Maybe I have a small syntax issue, or maybe I'm off base in my logic, I'm not really sure. The ultimate goal is to read the variables stored in SSM, but I haven't gotten to that point yet. Here is my code. The problem spot I'm having is near the end:

    Parameters:
      CanaryName:
        Type: String
        Default: my-canary
        MaxLength: 21
      HostName:
        Type: String
        Default: foo.bar.net
        MaxLength: 128
      Path:
        Type: String
        Default: /v1/status
        MaxLength: 256
      Port:
        Type: Number
        Default: 443
    
    
Resources:
  CloudWatchSyntheticsRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName:
        Fn::Sub: CloudWatchSyntheticsRole-${CanaryName}-${AWS::Region}
      Description: CloudWatch Synthetics lambda execution role for running canaries
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
            Condition: {}

 

     RolePermissions:
        Type: AWS::IAM::Policy
        Properties:
          Roles:
            - Ref: CloudWatchSyntheticsRole
          PolicyName:
            Fn::Sub: CloudWatchSyntheticsPolicy-${CanaryName}-${AWS::Region}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetBucketLocation
                Resource:
                  - Fn::Sub: arn:aws:s3:::${ResultsBucket}/*
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:CreateLogGroup
                Resource:
                  - Fn::Sub: arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/cwsyn-test-*
              - Effect: Allow
                Action:
                  - s3:ListAllMyBuckets
                Resource: '*'
              - Effect: Allow
                Resource: '*'
                Action: cloudwatch:PutMetricData
                Condition:
                  StringEquals:
                    cloudwatch:namespace: CloudWatchSynthetics
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: "arn:aws:secretsmanager:*:MYACCOUNT:secret:*"
    
    
      ResultsBucket:
        Type: AWS::S3::Bucket
        Properties:
          BucketName:
            Fn::Sub: cw-syn-results-${AWS::AccountId}-${AWS::Region}
          BucketEncryption:
            ServerSideEncryptionConfiguration:
              - ServerSideEncryptionByDefault:
                  SSEAlgorithm: AES256
        
          Canary:
            Type: AWS::Synthetics::Canary
            Properties:
              Name:
                Fn::Sub: ${CanaryName}
              Code:
                Handler: exports.handler
                Script: |
                  var synthetics = require('Synthetics');
                  const log = require('SyntheticsLogger');
                  const https = require('https');
                  const http = require('http');
        
                  const apiCanaryBlueprint = async function () {
                      const postData = "";
        
                      const verifyRequest = async function (requestOption) {
                        return new Promise((resolve, reject) => {
                          log.info("Making request with options: " + JSON.stringify(requestOption));
                          let req
                          if (requestOption.port === 443) {
                            req = https.request(requestOption);
                          } else {
                            req = http.request(requestOption);
                          }
                          req.on('response', (res) => {
                            log.info(`Status Code: ${res.statusCode}`)
                            log.info(`Response Headers: ${JSON.stringify(res.headers)}`)
                            // If the response status code is not a 2xx success code
                            if (res.statusCode < 200 || res.statusCode > 299) {
                               reject("Failed: " + requestOption.path);
                            }
                            res.on('data', (d) => {
                              log.info("Response: " + d);
                            });
                            res.on('end', () => {
                              resolve();
                            })
                          });
        
                          req.on('error', (error) => {
                            reject(error);
                          });
        
                          if (postData) {
                            req.write(postData);
                          }
                          req.end();
                        });
                      }
                      secret = "MYSECREYKEY";
                      const headers = {"Authorization":"Basic ${secret}"}
                      headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' ');

                  // PROBLEM SPOT: HOW TO ACCESS THE CFT PARAMETERS? 

                      const requestOptions = `"hostname" : { "!Ref" : "$HostName" }, "method" : "GET", "path" : { "!Ref" : "$Path" }, "port" : { "!Ref" : "$Port" }` 
                          requestOptions['headers'] = headers;
                          await verifyRequest(requestOptions);
                      };
            
                      exports.handler = async () => {
                          return await apiCanaryBlueprint();
                      };

2 Answers 2

3

You can use the intrinsic function Fn::Sub to substitute ${param} inside a inline code block:

Code: !Sub |
   console.log('handle event: ${param}');

Handler: exports.handler

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html#w2aac25c28c59c11

The documentation also describes how to escape the node.js string interpolation: ${!Literal}

Sign up to request clarification or add additional context in comments.

Comments

2

Probably not the best answer, but you could instead create those variables in parameter store, and then reference those in the code.

here is a way to do that: How to access the aws parameter store from a lambda using node.js and aws-sdk

I believe that since you are adding the code inline all references to the CF template would not work, so using something external would work.

1 Comment

I didn't explicitly call it out in my question, but this was the goal - to read it from SSM. I'll edit my question to reflect that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.