-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): hotswap deployments (#15748)
This is the first PR implementing the ["Accelerated personal deployments" RFC](https://github.com/aws/aws-cdk-rfcs/blob/master/text/0001-cdk-update.md). It adds a (boolean) `--hotswap` flag to the `deploy` command that attempts to perform a short-circuit deployment, updating the resource directly, and skipping CloudFormation. If we detect that the current change cannot be short-circuited (because it contains an infrastructure change to the CDK code, most likely), we fall back on performing a full CloudFormation deployment, same as if `cdk deploy` was called without the `--hotswap` flag. In this PR, the new switch supports only Lambda functions. Later PRs will add support for new resource types. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Showing
18 changed files
with
787 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import * as cfn_diff from '@aws-cdk/cloudformation-diff'; | ||
import * as cxapi from '@aws-cdk/cx-api'; | ||
import { CloudFormation } from 'aws-sdk'; | ||
import { ISDK, Mode, SdkProvider } from './aws-auth'; | ||
import { DeployStackResult } from './deploy-stack'; | ||
import { ChangeHotswapImpact, HotswapOperation, ListStackResources } from './hotswap/common'; | ||
import { isHotswappableLambdaFunctionChange } from './hotswap/lambda-functions'; | ||
import { CloudFormationStack } from './util/cloudformation'; | ||
|
||
/** | ||
* Perform a hotswap deployment, | ||
* short-circuiting CloudFormation if possible. | ||
* If it's not possible to short-circuit the deployment | ||
* (because the CDK Stack contains changes that cannot be deployed without CloudFormation), | ||
* returns `undefined`. | ||
*/ | ||
export async function tryHotswapDeployment( | ||
sdkProvider: SdkProvider, assetParams: { [key: string]: string }, | ||
cloudFormationStack: CloudFormationStack, stackArtifact: cxapi.CloudFormationStackArtifact, | ||
): Promise<DeployStackResult | undefined> { | ||
const currentTemplate = await cloudFormationStack.template(); | ||
const stackChanges = cfn_diff.diffTemplate(currentTemplate, stackArtifact.template); | ||
|
||
// resolve the environment, so we can substitute things like AWS::Region in CFN expressions | ||
const resolvedEnv = await sdkProvider.resolveEnvironment(stackArtifact.environment); | ||
const hotswappableChanges = findAllHotswappableChanges(stackChanges, { | ||
...assetParams, | ||
'AWS::Region': resolvedEnv.region, | ||
'AWS::AccountId': resolvedEnv.account, | ||
}); | ||
if (!hotswappableChanges) { | ||
// this means there were changes to the template that cannot be short-circuited | ||
return undefined; | ||
} | ||
|
||
// create a new SDK using the CLI credentials, because the default one will not work for new-style synthesis - | ||
// it assumes the bootstrap deploy Role, which doesn't have permissions to update Lambda functions | ||
const sdk = await sdkProvider.forEnvironment(resolvedEnv, Mode.ForWriting); | ||
// apply the short-circuitable changes | ||
await applyAllHotswappableChanges(sdk, stackArtifact, hotswappableChanges); | ||
|
||
return { noOp: hotswappableChanges.length === 0, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs, stackArtifact }; | ||
} | ||
|
||
function findAllHotswappableChanges( | ||
stackChanges: cfn_diff.TemplateDiff, assetParamsWithEnv: { [key: string]: string }, | ||
): HotswapOperation[] | undefined { | ||
const hotswappableResources = new Array<HotswapOperation>(); | ||
let foundNonHotswappableChange = false; | ||
stackChanges.resources.forEachDifference((logicalId: string, change: cfn_diff.ResourceDifference) => { | ||
const lambdaFunctionShortCircuitChange = isHotswappableLambdaFunctionChange(logicalId, change, assetParamsWithEnv); | ||
if (lambdaFunctionShortCircuitChange === ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT) { | ||
foundNonHotswappableChange = true; | ||
} else if (lambdaFunctionShortCircuitChange === ChangeHotswapImpact.IRRELEVANT) { | ||
// empty 'if' just for flow-aware typing to kick in... | ||
} else { | ||
hotswappableResources.push(lambdaFunctionShortCircuitChange); | ||
} | ||
}); | ||
return foundNonHotswappableChange ? undefined : hotswappableResources; | ||
} | ||
|
||
async function applyAllHotswappableChanges( | ||
sdk: ISDK, stackArtifact: cxapi.CloudFormationStackArtifact, hotswappableChanges: HotswapOperation[], | ||
): Promise<void[]> { | ||
// The current resources of the Stack. | ||
// We need them to figure out the physical name of a function in case it wasn't specified by the user. | ||
// We fetch it lazily, to save a service call, in case all updated Lambdas have their names set. | ||
const listStackResources = new LazyListStackResources(sdk, stackArtifact.stackName); | ||
|
||
return Promise.all(hotswappableChanges.map(hotswapOperation => hotswapOperation.apply(sdk, listStackResources))); | ||
} | ||
|
||
class LazyListStackResources implements ListStackResources { | ||
private stackResources: CloudFormation.StackResourceSummary[] | undefined; | ||
|
||
constructor(private readonly sdk: ISDK, private readonly stackName: string) { | ||
} | ||
|
||
async listStackResources(): Promise<CloudFormation.StackResourceSummary[]> { | ||
if (this.stackResources === undefined) { | ||
this.stackResources = await this.getStackResource(); | ||
} | ||
return this.stackResources; | ||
} | ||
|
||
private async getStackResource(): Promise<CloudFormation.StackResourceSummary[]> { | ||
const ret = new Array<CloudFormation.StackResourceSummary>(); | ||
let nextToken: string | undefined; | ||
do { | ||
const stackResourcesResponse = await this.sdk.cloudFormation().listStackResources({ | ||
StackName: this.stackName, | ||
NextToken: nextToken, | ||
}).promise(); | ||
ret.push(...(stackResourcesResponse.StackResourceSummaries ?? [])); | ||
nextToken = stackResourcesResponse.NextToken; | ||
} while (nextToken); | ||
return ret; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import * as cfn_diff from '@aws-cdk/cloudformation-diff'; | ||
import { CloudFormation } from 'aws-sdk'; | ||
import { ISDK } from '../aws-auth'; | ||
import { evaluateCfn } from '../util/cloudformation/evaluate-cfn'; | ||
|
||
export interface ListStackResources { | ||
listStackResources(): Promise<CloudFormation.StackResourceSummary[]>; | ||
} | ||
|
||
/** | ||
* An interface that represents a change that can be deployed in a short-circuit manner. | ||
*/ | ||
export interface HotswapOperation { | ||
apply(sdk: ISDK, stackResources: ListStackResources): Promise<any>; | ||
} | ||
|
||
/** | ||
* An enum that represents the result of detection whether a given change can be hotswapped. | ||
*/ | ||
export enum ChangeHotswapImpact { | ||
/** | ||
* This result means that the given change cannot be hotswapped, | ||
* and requires a full deployment. | ||
*/ | ||
REQUIRES_FULL_DEPLOYMENT = 'requires-full-deployment', | ||
|
||
/** | ||
* This result means that the given change can be safely be ignored when determining | ||
* whether the given Stack can be hotswapped or not | ||
* (for example, it's a change to the CDKMetadata resource). | ||
*/ | ||
IRRELEVANT = 'irrelevant', | ||
} | ||
|
||
export type ChangeHotswapResult = HotswapOperation | ChangeHotswapImpact; | ||
|
||
/** | ||
* For old-style synthesis which uses CFN Parameters, | ||
* the Code properties can have the values of complex CFN expressions. | ||
* For new-style synthesis of env-agnostic stacks, | ||
* the Fn::Sub expression is used for the Asset bucket. | ||
* Evaluate the CFN expressions to concrete string values which we need for the | ||
* updateFunctionCode() service call. | ||
*/ | ||
export function stringifyPotentialCfnExpression(value: any, assetParamsWithEnv: { [key: string]: string }): string { | ||
// if we already have a string, nothing to do | ||
if (value == null || typeof value === 'string') { | ||
return value; | ||
} | ||
|
||
// otherwise, we assume this is a CloudFormation expression that we need to evaluate | ||
return evaluateCfn(value, assetParamsWithEnv); | ||
} | ||
|
||
export function assetMetadataChanged(change: cfn_diff.ResourceDifference): boolean { | ||
return !!change.newValue?.Metadata['aws:asset:path']; | ||
} |
Oops, something went wrong.