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

feat(cli): Hotswapping Support for S3 Bucket Deployments #17638

Merged
merged 29 commits into from
Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
be597f3
stash
comcalvi Nov 17, 2021
bb2b80f
added tests and got a successful hotswap
comcalvi Nov 18, 2021
3ca6d7c
added support for all except the IAM policy change referring to a dif…
comcalvi Nov 19, 2021
f37bf6d
IAM Policy is now bound to the S3 deployment change
comcalvi Nov 19, 2021
f9eb43d
all tests passing
comcalvi Nov 23, 2021
f2675df
removed NONE, added an empty hotswap operation
comcalvi Nov 23, 2021
e382c52
updated comment
comcalvi Nov 23, 2021
8aaae29
added readme
comcalvi Nov 23, 2021
b662ae4
removed unneeded mocking of IAM SDK calls
comcalvi Nov 23, 2021
45edf7e
removed unneeded iam call
comcalvi Nov 23, 2021
240b70c
incorporated review comments and fixed broken tests that didn't refer…
comcalvi Nov 23, 2021
22c9131
removed eslint-disable
comcalvi Nov 23, 2021
29993e8
updated the way properties are passed to the lambda and removed needl…
comcalvi Nov 23, 2021
af9f35a
Send all requests to the Custom Resource Lambda, instead of just the …
skinny85 Nov 23, 2021
3e7f2d8
incorporated adam's code and fixed the failing lambda
comcalvi Nov 24, 2021
f9853fc
added test to confirm that undefined roles do not make the cli crash
comcalvi Nov 24, 2021
3dc72e8
refs to the lambda that are not s3 deployments result in full deployment
comcalvi Nov 24, 2021
13476ad
refactored IAM Policy Handling
comcalvi Nov 24, 2021
cf1e73f
updated test to have an additional role to verify that early returns …
comcalvi Nov 24, 2021
a2788c2
improved tests and caught case of multiple role references
comcalvi Dec 2, 2021
4866367
refactor
comcalvi Dec 2, 2021
7a2b96e
inlined policies
comcalvi Dec 3, 2021
43b9e53
improved testing
comcalvi Dec 3, 2021
451154a
Merge branch 'master' of https://github.com/aws/aws-cdk into bucket-h…
comcalvi Dec 3, 2021
df69b26
updated tests to incorporate latest from master
comcalvi Dec 3, 2021
302a955
addressed review comments
comcalvi Dec 9, 2021
3750683
added PolicyName property for correctness
comcalvi Dec 9, 2021
afcb3f5
policies that appear in the same template should have different names
comcalvi Dec 9, 2021
d582164
Merge branch 'master' into bucket-hotswap
mergify[bot] Dec 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ Hotswapping is currently supported for the following changes
- Code asset changes of AWS Lambda functions.
- Definition changes of AWS Step Functions State Machines.
- Container asset changes of AWS ECS Services.
- Website asset changes of AWS S3 Bucket Deployments
comcalvi marked this conversation as resolved.
Show resolved Hide resolved

**⚠ Note #1**: This command deliberately introduces drift in CloudFormation stacks in order to speed up deployments.
For this reason, only use it for development purposes.
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/api/hotswap-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, ListStackRe
import { isHotswappableEcsServiceChange } from './hotswap/ecs-services';
import { EvaluateCloudFormationTemplate } from './hotswap/evaluate-cloudformation-template';
import { isHotswappableLambdaFunctionChange } from './hotswap/lambda-functions';
import { isHotswappableS3BucketDeploymentChange } from './hotswap/s3-bucket-deployments';
import { isHotswappableStateMachineChange } from './hotswap/stepfunctions-state-machines';
import { CloudFormationStack } from './util/cloudformation';

Expand Down Expand Up @@ -73,6 +74,7 @@ async function findAllHotswappableChanges(
isHotswappableLambdaFunctionChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
isHotswappableStateMachineChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
isHotswappableEcsServiceChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
isHotswappableS3BucketDeploymentChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate),
]);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export class EvaluateCloudFormationTemplate {
return stackResources.find(sr => sr.LogicalResourceId === logicalId)?.PhysicalResourceId;
}

public async findLogicallNameFor(physicalId: string): Promise<string | undefined> {
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
const stackResources = await this.stackResources.listStackResources();
return stackResources.find(sr => sr.PhysicalResourceId === physicalId)?.LogicalResourceId;
}

public findReferencesTo(logicalId: string): Array<ResourceDefinition> {
const ret = new Array<ResourceDefinition>();
for (const [resourceLogicalId, resourceDef] of Object.entries(this.template?.Resources ?? {})) {
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk/lib/api/hotswap/lambda-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { assetMetadataChanged, ChangeHotswapImpact, ChangeHotswapResult, Hotswap
import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template';

/**
* Returns `false` if the change cannot be short-circuited,
* `true` if the change is irrelevant from a short-circuit perspective
* Returns `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` if the change cannot be short-circuited,
* `ChangeHotswapImpact.IRRELEVANT` if the change is irrelevant from a short-circuit perspective
* (like a change to CDKMetadata),
* or a LambdaFunctionResource if the change can be short-circuited.
*/
Expand Down
107 changes: 107 additions & 0 deletions packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ISDK } from '../aws-auth';
import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate, establishResourcePhysicalName } from './common';
import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template';

/**
* This means that the value is required to exist by CloudFormation's API (or our S3 Bucket Deployment Lambda)
* but the actual value specified is irrelevant
*/
export const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn';

export async function isHotswappableS3BucketDeploymentChange(
logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate,
): Promise<ChangeHotswapResult> {
// In old-style synthesis, the policy used by the lambda to copy assets Ref's the assets directly,
// meaning that the changes made to the Policy are artifacts that can be safely ignored
if (change.newValue.Type === 'AWS::IAM::Policy') {
const roles = change.newValue.Properties?.Roles;
for (const role of roles) {
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
const roleLogicalName = await evaluateCfnTemplate.findLogicallNameFor(await evaluateCfnTemplate.evaluateCfnExpression(role));
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
if (!roleLogicalName) {
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
}

const roleRefs = evaluateCfnTemplate.findReferencesTo(roleLogicalName);
for (const roleRef of roleRefs) {
if (roleRef.Type === 'AWS::Lambda::Function') {
const lambdaRefs = evaluateCfnTemplate.findReferencesTo(roleRef.LogicalId);
for (const lambdaRef of lambdaRefs) {
// If S3Deployment -> Lambda -> Role and IAM::Policy -> Role, then this IAM::Policy change is an
// artifact of old-style synthesis
if (lambdaRef.Type === 'Custom::CDKBucketDeployment') {
return new EmptyHotswapOperation();
}
}
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

if (change.newValue.Type !== 'Custom::CDKBucketDeployment') {
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
}

// note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either
const functionName = await establishResourcePhysicalName(logicalId, change.newValue.Properties?.ServiceToken, evaluateCfnTemplate);
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
if (!functionName) {
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
}

const sourceBucketNames = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.SourceBucketNames);
const sourceObjectKeys = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.SourceObjectKeys);
const destinationBucketName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.DestinationBucketName);
const destinationBucketKeyPrefix = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.DestinationBucketKeyPrefix);
comcalvi marked this conversation as resolved.
Show resolved Hide resolved

comcalvi marked this conversation as resolved.
Show resolved Hide resolved
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
return new S3BucketDeploymentHotswapOperation({
FunctionName: functionName,
SourceBucketNames: sourceBucketNames,
SourceObjectKeys: sourceObjectKeys,
DestinationBucketName: destinationBucketName,
DestinationBucketKeyPrefix: destinationBucketKeyPrefix,
});
}

interface S3BucketDeployment {
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
FunctionName: string,
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
SourceBucketNames: any,
SourceObjectKeys: any,
DestinationBucketName: any,
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
DestinationBucketKeyPrefix: string,
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
}

class S3BucketDeploymentHotswapOperation implements HotswapOperation {
constructor(private readonly s3BucketDeployment: S3BucketDeployment) {
}

public async apply(sdk: ISDK): Promise<any> {
const deployment = this.s3BucketDeployment;

return sdk.lambda().invoke({
FunctionName: deployment.FunctionName,
// Lambda refuses to take a direct JSON object and requires it to be stringify()'d
Payload: JSON.stringify({
RequestType: 'Update',
ResponseURL: REQUIRED_BY_CFN,
PhysicalResourceId: REQUIRED_BY_CFN,
StackId: REQUIRED_BY_CFN,
RequestId: REQUIRED_BY_CFN,
LogicalResourceId: REQUIRED_BY_CFN,
ResourceProperties: {
SourceBucketNames: deployment.SourceBucketNames,
SourceObjectKeys: deployment.SourceObjectKeys,
DestinationBucketName: deployment.DestinationBucketName,
DestinationBucketKeyPrefix: deployment.DestinationBucketKeyPrefix,
},
}),
}).promise();
}
}

class EmptyHotswapOperation implements HotswapOperation {
constructor() {
}

public async apply(sdk: ISDK): Promise<any> {
return new Promise(() => { sdk; });
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
}
}
9 changes: 8 additions & 1 deletion packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export function pushStackResourceSummaries(...items: CloudFormation.StackResourc
currentCfnStackResources.push(...items);
}

export function setCurrentCfnStackTemplate(template: Template) {
export function setCurrentCfnStackTemplate(_template: Template) {
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
let template = JSON.parse(JSON.stringify(_template)); // deep copy the _template, so our tests can mutate one template instead of creating two
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
currentCfnStack.setTemplate(template);
}

Expand Down Expand Up @@ -87,6 +88,12 @@ export class CfnMockProvider {
});
}

public setLambdaInvocationMock(mockInvoke: (input: lambda.InvocationRequest) => lambda.InvocationResponse) {
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
this.mockSdkProvider.stubLambda({
invoke: mockInvoke,
});
}

public stubEcs(stubs: SyncHandlerSubsetOf<AWS.ECS>, additionalProperties: { [key: string]: any } = {}): void {
this.mockSdkProvider.stubEcs(stubs, additionalProperties);
}
Expand Down
Loading