Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to call out to a custom Lambda function using AwsCustomResource and use the response to create resources? #7430

Closed
nicklaw5 opened this issue Apr 19, 2020 · 14 comments
Assignees
Labels
@aws-cdk/custom-resources Related to AWS CDK Custom Resources guidance Question that needs advice or information.

Comments

@nicklaw5
Copy link
Contributor

nicklaw5 commented Apr 19, 2020

❓ General Issue

The Question

What I want to do is call out to a custom API endpoint (which is exposed by a Lambda function), return the ID of an existing VPC and use it to create a security group. (This is a contrived example. I have other use cases for retrieving values from our custom API.)

I have the following basic my-func-name lambda for calling out to the API:

import axios from 'axios'

export const handler = async (event: any = {}): Promise<any> => {
  const resp = await axios(`https://my.api-example.com/${event.path}`)
  return resp.data
}

The below stack uses AwsCustomResource to invoke the above lambda and attempts to use the the response data to create a security group. But because AwsCustomResource.getResponseField returns a Token value, I'm unable to parse the JSON response into a Object, and instead receive an error due to it trying to parse the Token represented string.

const getDataByPath = (scope: Construct, path: string): string => {
  const lambdaInvokeSdkCall: AwsSdkCall = {
    service: 'Lambda',
    action: 'invoke',
    parameters: {
      FunctionName: 'my-func-name',
      Payload: `{"path": "${path}"}`,
    },
    physicalResourceId: PhysicalResourceId.of('InvokeLambdaResourceId1234'),
  }

  const acr = new AwsCustomResource(scope, 'MyAwsCustomResource', {
    policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
    onCreate: lambdaInvokeSdkCall,
  })

  // Example expected return payload:
  // {
  //   "vpc_dev": "vpc-1234abcd",
  //   "vpc_test": "vpc-2345bcde",
  //   "vpc_prod": "vpc-3456cdef"
  // }
  return JSON.parse(acr.getResponseField('Payload')) // !!! Fails here !!!
}

class MyStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id)

    const vpcIds = getDataByPath(this, 'vpcs/all')

    const vpc = Vpc.fromLookup(this, 'MyVpc', {
      vpcId: vpcIds.vpc_dev,
    })

    new SecurityGroup(scope, 'asdasd', { vpc })
  }
}

const app = new App()
new MyStack(app, 'MyStack')

Error:

Unexpected token $ in JSON at position 0
Subprocess exited with error 1
Error: Subprocess exited with error 1
at ChildProcess.<anonymous>(/app/node_modules / aws - cdk / lib / api / cxapp / exec.ts: 118: 23)
at ChildProcess.emit(events.js: 310: 20)
at ChildProcess.EventEmitter.emit(domain.js: 482: 12)
at Process.ChildProcess._handle.onexit(internal / child_process.js: 275: 12)

So I understand the issue here. Just want to understand how to get around it. How can I use the concrete response data from an invoked Lambda for creating arbitrary resources?

Environment

  • CDK CLI Version: 1.33.0
  • Module Version: 133.0
  • OS: all
  • Language: TypeScript

Other information

@nicklaw5 nicklaw5 added the needs-triage This issue or PR still needs to be triaged. label Apr 19, 2020
@NetaNir
Copy link
Contributor

NetaNir commented Apr 19, 2020

I think the docs on getResponseField address this
You should be able to use Token.aXxx.

@nicklaw5
Copy link
Contributor Author

Hi @NetaNir. Thanks for responding.

I have looked into Token, but I am a little confused on how to use the methods it provides. I wasn't able to find any examples either. Are you able to provide an example as to what you would do?

@jogold
Copy link
Contributor

jogold commented Apr 19, 2020

Short answer: use the provider framework for this

Longer answer: acr.getResponseField('Payload') is a Token because the value is only available at deploy time so you cannot JSON.parse it at synth time. You would have to parse it at deploy time and there's no CloudFormation instrinsic function directly available to do that. Using the provider framework you can reference your Lambda function, the framework will invoke it and JSON.parse the returned payload:

const jsonPayload = parseJsonPayload(resp.Payload);

@nicklaw5
Copy link
Contributor Author

Thank @jogold. I will give it a shot.

@nicklaw5
Copy link
Contributor Author

One issue with that approach is that I again need to know the name of the VPC before I can create the provider Lambda function, due to the fact the Lambda that hits the API needs to reside in the VPC.

Is there a way to reference a Lambda function that already exists out of the stack?

@nicklaw5
Copy link
Contributor Author

Ok, so I spoke too soon. Found this: #4810 (comment) - will give it a go.

@nicklaw5
Copy link
Contributor Author

nicklaw5 commented Apr 19, 2020

Unfortunately I'm still having the same issue using @jogold's suggestion of the provider framework. The problem being that the CustomResource.getAtt and CustomResource.getAttString methods, both return tokens, and not the concrete response from the Lambda function.

My code:

const onEvent = Function.fromFunctionArn(scope, 'MyFunc', 'arn:aws:lambda:ap-southeast-2:123456789:function:my-func-name')

const cr = new CustomResource(scope, 'MyCustomResource', {
  provider: CustomResourceProvider.fromLambda(onEvent),
  properties: {
    FunctionName: 'my-func-name',
    Payload: `{"path": "vpcs/all"}`,
  },
})

cr.getAtt('Payload')        // returns token
cr.getAttString('Payload')  // returns token

Can someone please share a concrete example on how I can invoke a Lambda and return the concrete response?

@jogold
Copy link
Contributor

jogold commented Apr 19, 2020

It's a token because it's a deploy-time value. You cannot use it at synth time but you can use the returned value in another construct. Also you can do cr.getAttString('vpc_dev') now.

What are you trying to achieve exactly? What kind of resources do you want to create?

Also, if everything is known at synth time then maybe you don't need a custom resource.

Looks like something for Stack Overflow.

@nicklaw5
Copy link
Contributor Author

nicklaw5 commented Apr 19, 2020

I am currently trying to achieve a very basic actions: create a security group using an existing VPC.

When trying to fetch an existing VPC using a token with the Vpc.fromLookup function I get the following error:

All arguments to Vpc.fromLookup() must be concrete (no Tokens)
Subprocess exited with error 1
Error: Subprocess exited with error 1
    at ChildProcess.<anonymous> (/app/node_modules/aws-cdk/lib/api/cxapp/exec.ts:118:23)
    at ChildProcess.emit (events.js:310:20)
    at ChildProcess.EventEmitter.emit (domain.js:482:12)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)

Which I assume means this function is executed at synth time(?). This is why I am trying to resolve the concrete data object from the Lambda.

Is there anyway I can call out to a HTTP API at synth time only? For example:

if (isSynth) {
  const resp = callCustomApi('/my/path')

  const vpc = Vpc.fromLookup(this, 'MyVpc', {
    vpcId: resp.vpc_dev,
  })
}

@jogold
Copy link
Contributor

jogold commented Apr 20, 2020

You cannot use .fromLookup() with a token:

throw new Error('All arguments to Vpc.fromLookup() must be concrete (no Tokens)');

This is because it calls the EC2 API at synth time to lookup all the attributes of the VPC to import.

You could use the Vpc.fromVpcAttributes(): either with known constants or connect it a custom resource. It requires that you specify availability zones and subnet ids as well. Another solution could be to "script" a call to your API, write the output to a JSON file and import this file in you CDK app. You would call this script before running your CDK app.

@nicklaw5
Copy link
Contributor Author

nicklaw5 commented Apr 20, 2020

Another solution could be to "script" a call to your API, write the output to a JSON file and import this file in you CDK app. You would call this script before running your CDK app.

The problem with that solution is that I don't know what values I need from the API prior to executing CDK. I could wrap the CDK executable with our own executable and have our executable run CDK as a child process. This I have tried but its not ideal.

What we are building is a Typescript library that wraps around CDK to apply our company-specific
resource opinions/defaults, naming conventions, and tagging rules amongst other things. This library will be used to deploy some 300+ applications and various infrastructure resources across some 25 AWS accounts. We use a few key environment variables to determine which account, VPC, subnets and availability zones resources are deployed to. Ideally what we want is to resolve these environment variables at synth time, and use them to call out to the API to retrieve their concrete values.

We currently achieve this scale of deployment through custom in-house built tool. However, it has become dated over the years and is difficult add functionality to or maintain. We are hoping that CDK can replace this tool and provide base framework for which to continue building from.

@SomayaB SomayaB added guidance Question that needs advice or information. @aws-cdk/custom-resources Related to AWS CDK Custom Resources labels Apr 20, 2020
@SomayaB SomayaB removed the needs-triage This issue or PR still needs to be triaged. label Apr 20, 2020
@nicklaw5
Copy link
Contributor Author

nicklaw5 commented Apr 20, 2020

Here is the current solution I'm leaning towards.

We would have a standalone call-api.js script which can get called using execSync from anyway in CDK. This script will call out the the API and return the stringified JSON response. We are then able to parse the JSON response into an JS object. Error handling as been left out of the below example for brevity. This seem like a bit of an anti-pattern, but its the only solution that I've liked during my testing.

call-api.js

#!/usr/bin/env node

const http = require('http')

http.get('https://my.api.com/vpcs/all', (resp) => {
  let data = ''

  // Concatenate all chunks
  resp.on('data', (chunk) => {
    data += chunk
  })

  // Send response
  resp.on('end', () => {
    // Returns the following JSON object.
    // {
    //   "vpc_dev": "vpc-1234abcd",
    //   "vpc_test": "vpc-2345bcde",
    //   "vpc_prod": "vpc-3456cdef"
    // }
    console.log(data)
  })
})

my-stack.ts

import { execSync } from 'child_process'
import { App, Stack } from '@aws-cdk/core'
import { Vpc, SecurityGroup } from '@aws-cdk/aws-ec2'

class MyStack extends Stack {
  constructor(scope: App) {
    super(scope, 'MyStack')

    const resp = execSync('node ./call-api.js').toString()
    const vpcId = JSON.parse(resp).vpc_dev

    const vpc = Vpc.fromLookup(this, 'MyVpc', { vpcId })
    new SecurityGroup(this, 'MySg', { vpc })
  }
}

const app = new App()
new MyStack(app)
app.synth()

Thoughts, comments or concerns?

@eladb eladb closed this as completed May 3, 2020
@eladb
Copy link
Contributor

eladb commented May 3, 2020

I am closing this since this is mostly guidance discussion. Ideally should be in Stack Overflow.

@jacido
Copy link

jacido commented Dec 6, 2023

@eladb Hi, I see this was dated from 2020. Is this method still usable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/custom-resources Related to AWS CDK Custom Resources guidance Question that needs advice or information.
Projects
None yet
Development

No branches or pull requests

6 participants