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

feature: make custom resource provider framework region aware #29957

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -23,8 +23,8 @@ import {
CORE_INTERNAL_STACK,
CORE_INTERNAL_CR_PROVIDER,
PATH_MODULE,
REGION_INFO,
} from './modules';
import { toLambdaRuntime } from './utils/framework-utils';

/**
* Initialization properties for a class constructor.
Expand Down Expand Up @@ -86,7 +86,7 @@ export abstract class HandlerFrameworkClass extends ClassType {
*/
public static buildFunction(scope: HandlerFrameworkModule, props: HandlerFrameworkClassProps): HandlerFrameworkClass {
return new (class Function extends HandlerFrameworkClass {
protected readonly externalModules = [PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE];
protected readonly externalModules = [PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE, REGION_INFO, CORE_MODULE];

public constructor() {
super(scope, {
Expand All @@ -97,11 +97,13 @@ export abstract class HandlerFrameworkClass extends ClassType {

this.importExternalModulesInto(scope);

this.buildCustomResourceRuntimeDeterminer();

const superProps = new ObjectLiteral([
new Splat(expr.ident('props')),
['code', expr.directCode(`lambda.Code.fromAsset(path.join(__dirname, '${props.codeDirectory}'))`)],
['handler', expr.lit(props.handler)],
['runtime', expr.directCode(toLambdaRuntime(props.runtime))],
['runtime', expr.directCode(`${this.name}.builtInCustomResourceNodeRuntime(scope)`)],
]);
this.buildConstructor({
constructorPropsType: LAMBDA_MODULE.FunctionOptions,
Expand All @@ -118,7 +120,7 @@ export abstract class HandlerFrameworkClass extends ClassType {
*/
public static buildSingletonFunction(scope: HandlerFrameworkModule, props: HandlerFrameworkClassProps): HandlerFrameworkClass {
return new (class SingletonFunction extends HandlerFrameworkClass {
protected readonly externalModules = [PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE];
protected readonly externalModules = [PATH_MODULE, CONSTRUCTS_MODULE, LAMBDA_MODULE, REGION_INFO, CORE_MODULE];

public constructor() {
super(scope, {
Expand All @@ -129,6 +131,8 @@ export abstract class HandlerFrameworkClass extends ClassType {

this.importExternalModulesInto(scope);

this.buildCustomResourceRuntimeDeterminer();

const uuid: PropertySpec = {
name: 'uuid',
type: Type.STRING,
Expand Down Expand Up @@ -163,7 +167,7 @@ export abstract class HandlerFrameworkClass extends ClassType {
new Splat(expr.ident('props')),
['code', expr.directCode(`lambda.Code.fromAsset(path.join(__dirname, '${props.codeDirectory}'))`)],
['handler', expr.lit(props.handler)],
['runtime', expr.directCode(toLambdaRuntime(props.runtime))],
['runtime', expr.directCode(`${this.name}.builtInCustomResourceNodeRuntime(scope)`)],
]);
this.buildConstructor({
constructorPropsType: _interface.type,
Expand All @@ -179,7 +183,7 @@ export abstract class HandlerFrameworkClass extends ClassType {
*/
public static buildCustomResourceProvider(scope: HandlerFrameworkModule, props: HandlerFrameworkClassProps): HandlerFrameworkClass {
return new (class CustomResourceProvider extends HandlerFrameworkClass {
protected readonly externalModules: ExternalModule[] = [PATH_MODULE, CONSTRUCTS_MODULE];
protected readonly externalModules: ExternalModule[] = [PATH_MODULE, CONSTRUCTS_MODULE, REGION_INFO];

public constructor() {
super(scope, {
Expand All @@ -197,6 +201,8 @@ export abstract class HandlerFrameworkClass extends ClassType {
}
this.importExternalModulesInto(scope);

this.buildCustomResourceProviderRuntimeDeterminer();

const getOrCreateMethod = this.addMethod({
name: 'getOrCreate',
static: true,
Expand Down Expand Up @@ -249,15 +255,15 @@ export abstract class HandlerFrameworkClass extends ClassType {
});
getOrCreateProviderMethod.addBody(
stmt.constVar(expr.ident('id'), expr.directCode('`${uniqueid}CustomResourceProvider`')),
stmt.constVar(expr.ident('stack'), expr.directCode('Stack.of(scope)')),
stmt.constVar(expr.ident('stack'), scope.coreInternal ? expr.directCode('Stack.of(scope)') : expr.directCode('core.Stack.of(scope)')),
stmt.constVar(expr.ident('existing'), expr.directCode(`stack.node.tryFindChild(id) as ${this.type}`)),
stmt.ret(expr.directCode(`existing ?? new ${this.name}(stack, id, props)`)),
);

const superProps = new ObjectLiteral([
new Splat(expr.ident('props')),
['codeDirectory', expr.directCode(`path.join(__dirname, '${props.codeDirectory}')`)],
['runtimeName', expr.lit(props.runtime)],
['runtimeName', expr.lit(`${this.name}.builtInCustomResourceProviderNodeRuntime(scope)`)],
]);
this.buildConstructor({
constructorPropsType: scope.coreInternal
Expand Down Expand Up @@ -296,11 +302,11 @@ export abstract class HandlerFrameworkClass extends ClassType {
return;
}
case CORE_MODULE.fqn: {
CORE_MODULE.importSelective(scope, [
scope.coreInternal ? CORE_MODULE.importSelective(scope, [
'Stack',
'CustomResourceProviderBase',
'CustomResourceProviderOptions',
]);
]) : CORE_MODULE.import(scope, 'core');
return;
}
case CORE_INTERNAL_CR_PROVIDER.fqn: {
Expand All @@ -318,6 +324,10 @@ export abstract class HandlerFrameworkClass extends ClassType {
LAMBDA_MODULE.import(scope, 'lambda');
return;
}
case REGION_INFO.fqn: {
REGION_INFO.importSelective(scope, ['FactName']);
return;
}
}
}

Expand Down Expand Up @@ -353,4 +363,38 @@ export abstract class HandlerFrameworkClass extends ClassType {
const superInitializerArgs: Expression[] = [scope, id, props.superProps];
init.addBody(new SuperInitializer(...superInitializerArgs));
}

private buildCustomResourceRuntimeDeterminer() {
const runtimeDeterminer = this.addMethod({
name: 'builtInCustomResourceNodeRuntime',
visibility: MemberVisibility.Private,
static: true,
returnType: LAMBDA_MODULE.Runtime,
});
runtimeDeterminer.addParameter({
name: 'scope',
type: CONSTRUCTS_MODULE.Construct,
});
runtimeDeterminer.addBody(
stmt.constVar(expr.ident('runtimeName'), expr.directCode('core.Stack.of(scope).regionalFact(FactName.DEFAULT_CR_NODE_VERSION, "nodejs18.x")')),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an API somewhere that will do a synth-time lookup if the region is known at synth time, or generate a runtime table for an agnostic stack. Not sure where it is again, but look it up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah regionalFact is the right API.

Put the hardcoded 18.x fallback in an obvious place somewhere, right next to the other table probably.

stmt.ret(expr.directCode('runtimeName ? new lambda.Runtime(runtimeName, lambda.RuntimeFamily.NODEJS, { supportsInlineCode: true }) : lambda.Runtime.NODEJS_18_X')),
);
}

private buildCustomResourceProviderRuntimeDeterminer() {
const runtimeDeterminer = this.addMethod({
name: 'builtInCustomResourceProviderNodeRuntime',
visibility: MemberVisibility.Private,
static: true,
returnType: Type.STRING,
});
runtimeDeterminer.addParameter({
name: 'scope',
type: CONSTRUCTS_MODULE.Construct,
});
runtimeDeterminer.addBody(
stmt.constVar(expr.ident('runtimeName'), expr.directCode('core.Stack.of(scope).regionalFact(FactName.DEFAULT_CR_NODE_VERSION, "nodejs18.x")')),
stmt.ret(expr.directCode('runtimeName ?? "nodejs18.x"')),
);
}
}
Expand Up @@ -46,15 +46,25 @@ class LambdaModule extends ExternalModule {
public readonly Function = Type.fromName(this, 'Function');
public readonly SingletonFunction = Type.fromName(this, 'SingletonFunction');
public readonly FunctionOptions = Type.fromName(this, 'FunctionOptions');
public readonly Runtime = Type.fromName(this, 'Runtime');

public constructor() {
super('../../../aws-lambda');
}
}

class RegionInfo extends ExternalModule {
public readonly FactName = Type.fromName(this, 'FactName');

public constructor() {
super('../../../region-info');
}
}

export const PATH_MODULE = new PathModule();
export const CONSTRUCTS_MODULE = new ConstructsModule();
export const CORE_MODULE = new CoreModule();
export const CORE_INTERNAL_STACK = new CoreInternalStack();
export const CORE_INTERNAL_CR_PROVIDER = new CoreInternalCustomResourceProvider();
export const LAMBDA_MODULE = new LambdaModule();
export const REGION_INFO = new RegionInfo();
Expand Up @@ -1709,3 +1709,39 @@ test('DeployTimeSubstitutedFile allows custom role to be supplied', () => {
},
});
});

test('region aware: gov cloud', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'RegionAware', { env: { region: 'us-gov-east-1' } });
const bucket = new s3.Bucket(stack, 'Dest');

// WHEN
new s3deploy.BucketDeployment(stack, 'Deployment', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket: bucket,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs18.x',
});
});

test('region aware: commercial region', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'RegionAware', { env: { region: 'us-east-1' } });
const bucket = new s3.Bucket(stack, 'Dest');

// WHEN
new s3deploy.BucketDeployment(stack, 'Deployment', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket: bucket,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs20.x',
});
});
8 changes: 8 additions & 0 deletions packages/aws-cdk-lib/region-info/build-tools/fact-tables.ts
Expand Up @@ -90,6 +90,14 @@ export const PARTITION_MAP: { [region: string]: Region } = {
'us-isob-': { partition: Partition.UsIsoB, domainSuffix: 'sc2s.sgov.gov' },
};

export const CR_DEFAULT_RUNTIME_MAP: Record<Partition, string> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs be the value that feeds into Runtime.NODEJS_LATEST. There is no way to do NODEJS_LATEST correctly with a hardcoded constant.

[Partition.Default]: 'nodejs20.x',
[Partition.Cn]: 'nodejs18.x',
[Partition.UsGov]: 'nodejs18.x',
[Partition.UsIso]: 'nodejs18.x',
[Partition.UsIsoB]: 'nodejs18.x',
};

// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-logging-bucket-permissions
// https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html#attach-bucket-policy
// Any not listed regions use the service principal "logdelivery.elasticloadbalancing.amazonaws.com"
Expand Down
Expand Up @@ -13,6 +13,7 @@ import {
PARAMS_AND_SECRETS_LAMBDA_LAYER_ARNS,
APPCONFIG_LAMBDA_LAYER_ARNS,
PARTITION_SAML_SIGN_ON_URL,
CR_DEFAULT_RUNTIME_MAP,
} from './fact-tables';
import { AWS_CDK_METADATA } from './metadata';
import {
Expand Down Expand Up @@ -85,6 +86,8 @@ export async function main(): Promise<void> {

registerFact(region, 'APPMESH_ECR_ACCOUNT', APPMESH_ECR_ACCOUNTS[region]);

registerFact(region, 'DEFAULT_CR_NODE_VERSION', CR_DEFAULT_RUNTIME_MAP[partition]);

registerFact(region, 'SAML_SIGN_ON_URL', PARTITION_SAML_SIGN_ON_URL[partition]);

const firehoseCidrBlock = FIREHOSE_CIDR_BLOCKS[region];
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk-lib/region-info/lib/fact.ts
Expand Up @@ -195,6 +195,11 @@ export class FactName {
*/
public static readonly SAML_SIGN_ON_URL = 'samlSignOnUrl';

/**
* The default NodeJS version used for custom resource function runtimes
*/
public static readonly DEFAULT_CR_NODE_VERSION = 'defaultCrNodeVersion';

/**
* The ARN of CloudWatch Lambda Insights for a version (e.g. 1.0.98.0)
*/
Expand Down