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();
                      };
BPS
  • 607
  • 8
  • 29

2 Answers2

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}

f7o
  • 663
  • 4
  • 8
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.

  • 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. – BPS Oct 26 '20 at 19:08