Skip to content

Commit

Permalink
feat!: removes fixed name from lambda function (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
udondan authored Mar 9, 2024
1 parent ebef7a8 commit 56e17ef
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 110 deletions.
8 changes: 0 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,3 @@ install:
@echo -e "$(TARGET_COLOR)Running install$(NO_COLOR)"
@npm clean-install --prefer-offline --cache .npm
@npm list

test: build
@echo -e "$(TARGET_COLOR)Running test$(NO_COLOR)"
@npm run test

test-update:
@echo -e "$(TARGET_COLOR)Running test-update$(NO_COLOR)"
@npm run test -- -u
194 changes: 94 additions & 100 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
TagType,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import path = require('path');
import * as path from 'path';

const resourceType = 'Custom::EC2-Key-Pair';
const ID = `CFN::Resource::${resourceType}`;
Expand Down Expand Up @@ -130,6 +130,17 @@ export interface KeyPairProps extends ResourceProps {
* @default Name of the stack
*/
readonly resourcePrefix?: string;

/**
* Whether to use the legacy name for the Lambda function, which backs the custom resource.
*
* Starting with v4 of this package, the Lambda function by default has no longer a fixed name.
*
* If you migrate from v3 to v4, you need to set this to `true` as CloudFormation does not allow to change the name of the Lambda function used by custom resource.
*
* @default false
*/
readonly legacyLambdaName?: boolean;
}

/**
Expand Down Expand Up @@ -204,14 +215,18 @@ export class KeyPair extends Construct implements ITaggable {
}

const stack = Stack.of(this).stackName;
this.prefix = props.resourcePrefix ?? stack;
if (this.prefix.length + cleanID.length > 62)
// Cloudformation limits names to 63 characters.
Annotations.of(this).addError(
`Cloudformation limits names to 63 characters.
Prefix ${this.prefix} is too long to be used as a prefix for your roleName. Define parameter resourcePrefix?:`
);
this.lambda = this.ensureLambda();

if (props.legacyLambdaName) {
this.prefix = props.resourcePrefix ?? stack;
if (this.prefix.length + cleanID.length > 62) {
// Cloudformation limits names to 63 characters.
Annotations.of(this).addError(
`Cloudformation limits names to 63 characters.
Prefix ${this.prefix} is too long to be used as a prefix for your roleName. Define parameter resourcePrefix?:`
);
}
}
this.lambda = this.ensureLambda(props.legacyLambdaName || false);

this.tags = new TagManager(TagType.MAP, 'Custom::EC2-Key-Pair');
this.tags.setTag(createdByTag, ID);
Expand Down Expand Up @@ -265,108 +280,84 @@ export class KeyPair extends Construct implements ITaggable {
this.keyPairID = key.getAttString('KeyPairID');
}

private ensureLambda(): aws_lambda.Function {
private ensureLambda(legacyLambdaName: boolean): aws_lambda.Function {
const stack = Stack.of(this);
const constructName = 'EC2-Key-Name-Manager-Lambda';
const constructName = legacyLambdaName
? 'EC2-Key-Name-Manager-Lambda' // this name was not intentional but we keep it for legacy resources
: 'EC2-Key-Pair-Manager-Lambda';
const existing = stack.node.tryFindChild(constructName);
if (existing) {
return existing as aws_lambda.Function;
}

const resources = [`arn:${stack.partition}:ec2:*:*:key-pair/*`];

const policy = new aws_iam.ManagedPolicy(
stack,
'EC2-Key-Pair-Manager-Policy',
{
description: `Used by Lambda ${cleanID}, which is a custom CFN resource, managing EC2 Key Pairs`,
statements: [
new aws_iam.PolicyStatement({
actions: ['ec2:DescribeKeyPairs'],
resources: ['*'],
}),
new aws_iam.PolicyStatement({
actions: [
'ec2:CreateKeyPair',
'ec2:CreateTags',
'ec2:ImportKeyPair',
],
conditions: {
StringLike: {
'aws:RequestTag/CreatedByCfnCustomResource': ID,
},
},
resources,
}),
new aws_iam.PolicyStatement({
// allow delete/update, only if createdByTag is set
actions: ['ec2:CreateTags', 'ec2:DeleteKeyPair', 'ec2:DeleteTags'],
conditions: {
StringLike: {
'ec2:ResourceTag/CreatedByCfnCustomResource': ID,
},
},
resources,
}),

new aws_iam.PolicyStatement({
// we need this to check if a secret exists before attempting to delete it
actions: ['secretsmanager:ListSecrets'],
resources: ['*'],
}),
new aws_iam.PolicyStatement({
actions: [
'secretsmanager:CreateSecret',
'secretsmanager:TagResource',
],
conditions: {
StringLike: {
'aws:RequestTag/CreatedByCfnCustomResource': ID,
},
},
resources: ['*'],
}),
new aws_iam.PolicyStatement({
// allow delete/update, only if createdByTag is set
actions: [
'secretsmanager:DeleteResourcePolicy',
'secretsmanager:DeleteSecret',
'secretsmanager:DescribeSecret',
'secretsmanager:GetResourcePolicy',
'secretsmanager:GetSecretValue',
'secretsmanager:ListSecretVersionIds',
'secretsmanager:PutResourcePolicy',
'secretsmanager:PutSecretValue',
'secretsmanager:RestoreSecret',
'secretsmanager:UntagResource',
'secretsmanager:UpdateSecret',
'secretsmanager:UpdateSecretVersionStage',
],
conditions: {
StringLike: {
'secretsmanager:ResourceTag/CreatedByCfnCustomResource': ID,
},
},
resources: ['*'],
}),
const statements = [
new aws_iam.PolicyStatement({
actions: ['ec2:DescribeKeyPairs'],
resources: ['*'],
}),
new aws_iam.PolicyStatement({
actions: ['ec2:CreateKeyPair', 'ec2:CreateTags', 'ec2:ImportKeyPair'],
conditions: {
StringLike: {
'aws:RequestTag/CreatedByCfnCustomResource': ID,
},
},
resources,
}),
new aws_iam.PolicyStatement({
// allow delete/update, only if createdByTag is set
actions: ['ec2:CreateTags', 'ec2:DeleteKeyPair', 'ec2:DeleteTags'],
conditions: {
StringLike: {
'ec2:ResourceTag/CreatedByCfnCustomResource': ID,
},
},
resources,
}),

new aws_iam.PolicyStatement({
// we need this to check if a secret exists before attempting to delete it
actions: ['secretsmanager:ListSecrets'],
resources: ['*'],
}),
new aws_iam.PolicyStatement({
actions: ['secretsmanager:CreateSecret', 'secretsmanager:TagResource'],
conditions: {
StringLike: {
'aws:RequestTag/CreatedByCfnCustomResource': ID,
},
},
resources: ['*'],
}),
new aws_iam.PolicyStatement({
// allow delete/update, only if createdByTag is set
actions: [
'secretsmanager:DeleteResourcePolicy',
'secretsmanager:DeleteSecret',
'secretsmanager:DescribeSecret',
'secretsmanager:GetResourcePolicy',
'secretsmanager:GetSecretValue',
'secretsmanager:ListSecretVersionIds',
'secretsmanager:PutResourcePolicy',
'secretsmanager:PutSecretValue',
'secretsmanager:RestoreSecret',
'secretsmanager:UntagResource',
'secretsmanager:UpdateSecret',
'secretsmanager:UpdateSecretVersionStage',
],
}
);

const role = new aws_iam.Role(stack, 'EC2-Key-Pair-Manager-Role', {
description: `Used by Lambda ${cleanID}, which is a custom CFN resource, managing EC2 Key Pairs`,
assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
policy,
aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSLambdaBasicExecutionRole'
),
],
});
conditions: {
StringLike: {
'secretsmanager:ResourceTag/CreatedByCfnCustomResource': ID,
},
},
resources: ['*'],
}),
];

const fn = new aws_lambda.Function(stack, constructName, {
functionName: `${this.prefix}-${cleanID}`,
role: role,
functionName: legacyLambdaName ? `${this.prefix}-${cleanID}` : undefined,
description: 'Custom CFN resource: Manage EC2 Key Pairs',
runtime: aws_lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
Expand All @@ -375,6 +366,9 @@ export class KeyPair extends Construct implements ITaggable {
),
timeout: Duration.minutes(lambdaTimeout),
});
statements.forEach((statement) => {
fn.role?.addToPrincipalPolicy(statement);
});

return fn;
}
Expand Down
25 changes: 23 additions & 2 deletions test/bin/test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
#!/usr/bin/env node
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
import {
GetCallerIdentityCommand,
STSClient,
STSClientConfig,
} from '@aws-sdk/client-sts';
import cdk = require('aws-cdk-lib');

import { TestStack } from '../lib/test-stack';

const region = 'us-east-1';

const clientConfig: STSClientConfig = {
region,
};
if (
process.env.AWS_ACCESS_KEY_ID &&
process.env.AWS_SECRET_ACCESS_KEY &&
process.env.AWS_SESSION_TOKEN
) {
clientConfig.credentials = {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN,
};
}

async function getIdentity() {
const stsClient = new STSClient({});
const stsClient = new STSClient(clientConfig);
const callerIdentity = await stsClient.send(new GetCallerIdentityCommand({}));
return callerIdentity.Arn?.split('/')[1] as string;
}
Expand Down
1 change: 1 addition & 0 deletions test/lib/test-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class TestStack extends cdk.Stack {
exposePublicKey: true,
storePublicKey: true,
publicKeyFormat: PublicKeyFormat.PEM,
legacyLambdaName: true,
});

const currentUser = cdk.aws_iam.User.fromUserName(
Expand Down

0 comments on commit 56e17ef

Please sign in to comment.