aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.64k stars 3.91k forks source link

feat(cloudfront): parameterise CloudFront Functions via find/replace #30492

Open SydneyUni-Jim opened 4 months ago

SydneyUni-Jim commented 4 months ago

Describe the feature

CloudFront Functions (not Lambda@Edge) don't provide a way to parameterise the code, in the way environment variables can with Lambda@Edge, for example. Allow code to be replaced with values from the CDK app during synth.

Use Case

Proposed Solution

In aws-cdk-lib/aws-cloudfront/lib/function.ts:

This is a naïve find (all) and replace. It does not attempt to parse the function code to find appropriate tokens. It does not attempt to replace code meaningfully. There is no guarantee that syntactically valid code remains syntactically valid after the find/replace.

Proof of concept

This is the code I'm currently using to get the KVS ID into a CloudFront Function. The proposed solution is a generalisation of this. ```typescript interface FileCodeWithKvsIdSubstitutionOptions extends cloudfront.FileCodeOptions { readonly keyValueStore: cloudfront.IKeyValueStore } class FileCodeWithKvsIdSubstitution extends cloudfront.FunctionCode { constructor(private options: FileCodeWithKvsIdSubstitutionOptions) { super() } public render(): string { return ( fs.readFileSync(this.options.filePath, { encoding: 'utf-8' }) .replace('«KVS_ID»', cdk.Token.asString(this.options.keyValueStore.keyValueStoreId)) ) } } ```

Example of using the proposed solution

Given this code for a CloudFront Function.

import cf from 'cloudfront'

// __IS_PROD__ anywhere in the code is replaced with true or false during CDK synth 

const kvs = cf.kvs('__KVS_ID__')  // __KVS_ID__ is replaced during CDK synth

async function handler(event) {
  console.log('is production: __IS_PROD__')
  // …
  if (__IS_PROD__) {
   // …
  }
  // …
  const v = __IS_PROD__ ? x : y
  // …
}

The use of double underscore delimiters is not part of the proposed solution. It would up to the CDK builder to create code that can be unambiguously found. If any delimiters are used, those delimiters would be part of the value for find.

__KVS_ID and __IS_PROD could be replaced in the code with this.

new cloudfront.Function(this, 'Function', {
  code: FunctionCode.fromFile({
    filePath: 'cloudfront-function.js',
    findReplace: [
      { find: '__KVS_ID__',  replace: keyValueStore.keyValueStoreId },
      { find: '__IS_PROD__', replace: `${ !!isProd }`, all: true },            // replace has to be string
    ],
  }),
})

If the KVS id is 61736184-1ad9-4b1b-9466-6ca1fb629685 and isProd is false, the code that would be sent to CloudFront would be this.

import cf from 'cloudfront'

// false anywhere in the code is replaced with true or false during CDK synth 

const kvs = cf.kvs('61736184-1ad9-4b1b-9466-6ca1fb629685')  // 61736184-1ad9-4b1b-9466-6ca1fb629685 is replaced during CDK synth

async function handler(event) {
  console.log('is production: false')
  // …
  if (false) {
   // …
  }
  // …
  const v = false ? x : y
  // …
}

Other Information

The Key Value Store can be used for parameterisation. But there's costs associated with that. And there's the conundrum of how to get the KVS ID into the CloudFront Function to begin with.

Acknowledgements

CDK version used

2.145.0

Environment details (OS name and version, etc.)

NodeJS/iron

SydneyUni-Jim commented 4 months ago

It would be great if find could be a JavaScript regular expression or a string. And it would be great if replace could be any, with findReplace converting anything not a string into a string. But I don't know if that's possible with JSII.

SydneyUni-Jim commented 4 months ago

Possible implementation of findReplace.

protected findReplace(code: string, findReplaceArr?: FindReplace[]): string {
  if (!findReplaceArr?.length) return code
  function reducer(acc: string, opt: FindReplace) {
    const r: string = typeof opt.replace === 'string' ? opt.replace : cdk.Token.asString(opt.replace)
    return opt.all ? acc.replaceAll(opt.find, r) : acc.replace(opt.find, r)
  }
  return findReplaceArr.reduce(reducer, code)
}
khushail commented 4 months ago

Hi @SydneyUni-Jim , thanks for reaching out. I see this readme doc talks about Key-value store ,quite similar to what you have proposed in solution. I might not fully comprehend about the solution but we are open to suggestions and please feel free to submit a PR !

SydneyUni-Jim commented 4 months ago

HI @khushail. Thanks for pointing out the docs. Unfortunately that only associates the Key Value Store with the function. It doesn't make the store's id available to the function's code. I will work on a PR.