-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
cloudformation-deployments.ts
322 lines (275 loc) · 9.84 KB
/
cloudformation-deployments.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import * as cxapi from '@aws-cdk/cx-api';
import { AssetManifest } from 'cdk-assets';
import { Tag } from '../cdk-toolkit';
import { debug } from '../logging';
import { publishAssets } from '../util/asset-publishing';
import { Mode, SdkProvider } from './aws-auth';
import { deployStack, DeployStackResult, destroyStack } from './deploy-stack';
import { ToolkitInfo } from './toolkit-info';
import { CloudFormationStack, Template } from './util/cloudformation';
import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
/**
* Replace the {ACCOUNT} and {REGION} placeholders in all strings found in a complex object.
*/
export async function replaceEnvPlaceholders<A extends { }>(object: A, env: cxapi.Environment, sdkProvider: SdkProvider): Promise<A> {
return cxapi.EnvironmentPlaceholders.replaceAsync(object, {
accountId: () => Promise.resolve(env.account),
region: () => Promise.resolve(env.region),
partition: async () => {
// There's no good way to get the partition!
// We should have had it already, except we don't.
//
// Best we can do is ask the "base credentials" for this environment for their partition. Cross-partition
// AssumeRole'ing will never work anyway, so this answer won't be wrong (it will just be slow!)
return (await sdkProvider.baseCredentialsPartition(env, Mode.ForReading)) ?? 'aws';
},
});
}
export interface DeployStackOptions {
/**
* Stack to deploy
*/
stack: cxapi.CloudFormationStackArtifact;
/**
* Execution role for the deployment (pass through to CloudFormation)
*
* @default - Current role
*/
roleArn?: string;
/**
* Topic ARNs to send a message when deployment finishes (pass through to CloudFormation)
*
* @default - No notifications
*/
notificationArns?: string[];
/**
* Override name under which stack will be deployed
*
* @default - Use artifact default
*/
deployName?: string;
/**
* Don't show stack deployment events, just wait
*
* @default false
*/
quiet?: boolean;
/**
* Name of the toolkit stack, if not the default name
*
* @default 'CDKToolkit'
*/
toolkitStackName?: string;
/**
* List of asset IDs which should NOT be built or uploaded
*
* @default - Build all assets
*/
reuseAssets?: string[];
/**
* Stack tags (pass through to CloudFormation)
*/
tags?: Tag[];
/**
* Stage the change set but don't execute it
*
* @default - false
*/
execute?: boolean;
/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;
/**
* Force deployment, even if the deployed template is identical to the one we are about to deploy.
* @default false deployment will be skipped if the template is identical
*/
force?: boolean;
/**
* Extra parameters for CloudFormation
* @default - no additional parameters will be passed to the template
*/
parameters?: { [name: string]: string | undefined };
/**
* Use previous values for unspecified parameters
*
* If not set, all parameters must be specified for every deployment.
*
* @default true
*/
usePreviousParameters?: boolean;
/**
* Display mode for stack deployment progress.
*
* @default - StackActivityProgress.Bar - stack events will be displayed for
* the resource currently being deployed.
*/
progress?: StackActivityProgress;
/**
* Whether we are on a CI system
*
* @default false
*/
readonly ci?: boolean;
/**
* Rollback failed deployments
*
* @default true
*/
readonly rollback?: boolean;
/*
* Whether to perform a 'hotswap' deployment.
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - false for regular deployments, true for 'watch' deployments
*/
readonly hotswap?: boolean;
}
export interface DestroyStackOptions {
stack: cxapi.CloudFormationStackArtifact;
deployName?: string;
roleArn?: string;
quiet?: boolean;
force?: boolean;
}
export interface StackExistsOptions {
stack: cxapi.CloudFormationStackArtifact;
deployName?: string;
}
export interface ProvisionerProps {
sdkProvider: SdkProvider;
}
/**
* Helper class for CloudFormation deployments
*
* Looks us the right SDK and Bootstrap stack to deploy a given
* stack artifact.
*/
export class CloudFormationDeployments {
private readonly sdkProvider: SdkProvider;
constructor(props: ProvisionerProps) {
this.sdkProvider = props.sdkProvider;
}
public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> {
debug(`Reading existing template for stack ${stackArtifact.displayName}.`);
const { stackSdk } = await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading);
const cfn = stackSdk.cloudFormation();
const stack = await CloudFormationStack.lookup(cfn, stackArtifact.stackName);
return stack.template();
}
public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
const { stackSdk, resolvedEnvironment, cloudFormationRoleArn } = await this.prepareSdkFor(options.stack, options.roleArn);
const toolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, stackSdk, options.toolkitStackName);
// Publish any assets before doing the actual deploy
await this.publishStackAssets(options.stack, toolkitInfo);
// Do a verification of the bootstrap stack version
await this.validateBootstrapStackVersion(
options.stack.stackName,
options.stack.requiresBootstrapStackVersion,
options.stack.bootstrapStackVersionSsmParameter,
toolkitInfo);
return deployStack({
stack: options.stack,
resolvedEnvironment,
deployName: options.deployName,
notificationArns: options.notificationArns,
quiet: options.quiet,
sdk: stackSdk,
sdkProvider: this.sdkProvider,
roleArn: cloudFormationRoleArn,
reuseAssets: options.reuseAssets,
toolkitInfo,
tags: options.tags,
execute: options.execute,
changeSetName: options.changeSetName,
force: options.force,
parameters: options.parameters,
usePreviousParameters: options.usePreviousParameters,
progress: options.progress,
ci: options.ci,
rollback: options.rollback,
hotswap: options.hotswap,
});
}
public async destroyStack(options: DestroyStackOptions): Promise<void> {
const { stackSdk, cloudFormationRoleArn: roleArn } = await this.prepareSdkFor(options.stack, options.roleArn);
return destroyStack({
sdk: stackSdk,
roleArn,
stack: options.stack,
deployName: options.deployName,
quiet: options.quiet,
});
}
public async stackExists(options: StackExistsOptions): Promise<boolean> {
const { stackSdk } = await this.prepareSdkFor(options.stack, undefined, Mode.ForReading);
const stack = await CloudFormationStack.lookup(stackSdk.cloudFormation(), options.deployName ?? options.stack.stackName);
return stack.exists;
}
/**
* Get the environment necessary for touching the given stack
*
* Returns the following:
*
* - The resolved environment for the stack (no more 'unknown-account/unknown-region')
* - SDK loaded with the right credentials for calling `CreateChangeSet`.
* - The Execution Role that should be passed to CloudFormation.
*/
private async prepareSdkFor(stack: cxapi.CloudFormationStackArtifact, roleArn?: string, mode = Mode.ForWriting) {
if (!stack.environment) {
throw new Error(`The stack ${stack.displayName} does not have an environment`);
}
const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment);
// Substitute any placeholders with information about the current environment
const arns = await replaceEnvPlaceholders({
assumeRoleArn: stack.assumeRoleArn,
// Use the override if given, otherwise use the field from the stack
cloudFormationRoleArn: roleArn ?? stack.cloudFormationExecutionRoleArn,
}, resolvedEnvironment, this.sdkProvider);
const stackSdk = await this.sdkProvider.forEnvironment(resolvedEnvironment, mode, {
assumeRoleArn: arns.assumeRoleArn,
assumeRoleExternalId: stack.assumeRoleExternalId,
});
return {
stackSdk,
resolvedEnvironment,
cloudFormationRoleArn: arns.cloudFormationRoleArn,
};
}
/**
* Publish all asset manifests that are referenced by the given stack
*/
private async publishStackAssets(stack: cxapi.CloudFormationStackArtifact, toolkitInfo: ToolkitInfo) {
const stackEnv = await this.sdkProvider.resolveEnvironment(stack.environment);
const assetArtifacts = stack.dependencies.filter(isAssetManifestArtifact);
for (const assetArtifact of assetArtifacts) {
await this.validateBootstrapStackVersion(
stack.stackName,
assetArtifact.requiresBootstrapStackVersion,
assetArtifact.bootstrapStackVersionSsmParameter,
toolkitInfo);
const manifest = AssetManifest.fromFile(assetArtifact.file);
await publishAssets(manifest, this.sdkProvider, stackEnv);
}
}
/**
* Validate that the bootstrap stack has the right version for this stack
*/
private async validateBootstrapStackVersion(
stackName: string,
requiresBootstrapStackVersion: number | undefined,
bootstrapStackVersionSsmParameter: string | undefined,
toolkitInfo: ToolkitInfo) {
if (requiresBootstrapStackVersion === undefined) { return; }
try {
await toolkitInfo.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter);
} catch (e) {
throw new Error(`${stackName}: ${e.message}`);
}
}
}
function isAssetManifestArtifact(art: cxapi.CloudArtifact): art is cxapi.AssetManifestArtifact {
return art instanceof cxapi.AssetManifestArtifact;
}