Skip to content

Commit 2256680

Browse files
authored
feat(cli): diff now uses the lookup Role for new-style synthesis (#18277)
This PR exposes information on the bootstrap lookup role on the CloudFormation stack artifact. This enables the CLI to assume the lookup role during cli operations in order to lookup information in the stack account. Along with the ARN of the lookup role, this also exposes a `requiresBootstrapStackVersion` property which is set to `8` (the version the lookup role was given ReadOnlyAccess), and the `bootstrapStackVersionSsmParameter` which is needed to lookup the bootstrap version if a user has renamed the bootstrap stack. This allows us to first check whether the lookupRole exists and has the correct permissions prior to using it. This also updates the `diff` capability in the CLI (run as part of `cdk diff` or `cdk deploy`) to use this new functionality. It now will try to assume the lookupRole and if it doesn't exist or if the bootstrap stack version is not valid, then it will fallback to using the deployRole (what it uses currently). This PR also updates the `forEnvironment` function to return whether or not it is returning the default credentials. This allows the calling function to decide whether or not it actually wants to use the default credentials. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d94d9ce commit 2256680

28 files changed

+569
-67
lines changed

packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts

+40
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
11

2+
/**
3+
* Information needed to access an IAM role created
4+
* as part of the bootstrap process
5+
*/
6+
export interface BootstrapRole {
7+
/**
8+
* The ARN of the IAM role created as part of bootrapping
9+
* e.g. lookupRoleArn
10+
*/
11+
readonly arn: string;
12+
13+
/**
14+
* External ID to use when assuming the bootstrap role
15+
*
16+
* @default - No external ID
17+
*/
18+
readonly assumeRoleExternalId?: string;
19+
20+
/**
21+
* Version of bootstrap stack required to use this role
22+
*
23+
* @default - No bootstrap stack required
24+
*/
25+
readonly requiresBootstrapStackVersion?: number;
26+
27+
/**
28+
* Name of SSM parameter with bootstrap stack version
29+
*
30+
* @default - Discover SSM parameter by reading stack
31+
*/
32+
readonly bootstrapStackVersionSsmParameter?: string;
33+
}
34+
235
/**
336
* Artifact properties for CloudFormation stacks.
437
*/
@@ -56,6 +89,13 @@ export interface AwsCloudFormationStackProperties {
5689
*/
5790
readonly cloudFormationExecutionRoleArn?: string;
5891

92+
/**
93+
* The role to use to look up values from the target AWS account
94+
*
95+
* @default - No role is assumed (current credentials are used)
96+
*/
97+
readonly lookupRole?: BootstrapRole;
98+
5999
/**
60100
* If the stack template has already been included in the asset manifest, its asset URL
61101
*

packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json

+30-1
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@
307307
"description": "The role that is passed to CloudFormation to execute the change set (Default - No role is passed (currently assumed role/credentials are used))",
308308
"type": "string"
309309
},
310+
"lookupRole": {
311+
"description": "The role to use to look up values from the target AWS account (Default - No role is assumed (current credentials are used))",
312+
"$ref": "#/definitions/BootstrapRole"
313+
},
310314
"stackTemplateAssetObjectUrl": {
311315
"description": "If the stack template has already been included in the asset manifest, its asset URL (Default - Not uploaded yet, upload just before deploying)",
312316
"type": "string"
@@ -328,6 +332,31 @@
328332
"templateFile"
329333
]
330334
},
335+
"BootstrapRole": {
336+
"description": "Information needed to access an IAM role created\nas part of the bootstrap process",
337+
"type": "object",
338+
"properties": {
339+
"arn": {
340+
"description": "The ARN of the IAM role created as part of bootrapping\ne.g. lookupRoleArn",
341+
"type": "string"
342+
},
343+
"assumeRoleExternalId": {
344+
"description": "External ID to use when assuming the bootstrap role (Default - No external ID)",
345+
"type": "string"
346+
},
347+
"requiresBootstrapStackVersion": {
348+
"description": "Version of bootstrap stack required to use this role (Default - No bootstrap stack required)",
349+
"type": "number"
350+
},
351+
"bootstrapStackVersionSsmParameter": {
352+
"description": "Name of SSM parameter with bootstrap stack version (Default - Discover SSM parameter by reading stack)",
353+
"type": "string"
354+
}
355+
},
356+
"required": [
357+
"arn"
358+
]
359+
},
331360
"AssetManifestProperties": {
332361
"description": "Artifact properties for the Asset Manifest",
333362
"type": "object",
@@ -598,7 +627,7 @@
598627
}
599628
},
600629
"returnAsymmetricSubnets": {
601-
"description": "Whether to populate the subnetGroups field of the {@link VpcContextResponse},\nwhich contains potentially asymmetric subnet groups.",
630+
"description": "Whether to populate the subnetGroups field of the{@linkVpcContextResponse},\nwhich contains potentially asymmetric subnet groups.",
602631
"default": false,
603632
"type": "boolean"
604633
},
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"15.0.0"}
1+
{"version":"16.0.0"}

packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts

+33
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier';
2121
*/
2222
const MIN_BOOTSTRAP_STACK_VERSION = 6;
2323

24+
/**
25+
* The minimum bootstrap stack version required
26+
* to use the lookup role.
27+
*/
28+
const MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION = 8;
29+
2430
/**
2531
* Configuration properties for DefaultStackSynthesizer
2632
*/
@@ -91,6 +97,25 @@ export interface DefaultStackSynthesizerProps {
9197
*/
9298
readonly lookupRoleArn?: string;
9399

100+
/**
101+
* External ID to use when assuming lookup role
102+
*
103+
* @default - No external ID
104+
*/
105+
readonly lookupRoleExternalId?: string;
106+
107+
/**
108+
* Use the bootstrapped lookup role for (read-only) stack operations
109+
*
110+
* Use the lookup role when performing a `cdk diff`. If set to `false`, the
111+
* `deploy role` credentials will be used to perform a `cdk diff`.
112+
*
113+
* Requires bootstrap stack version 8.
114+
*
115+
* @default true
116+
*/
117+
readonly useLookupRoleForStackOperations?: boolean;
118+
94119
/**
95120
* External ID to use when assuming role for image asset publishing
96121
*
@@ -269,6 +294,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer {
269294
private fileAssetPublishingRoleArn?: string;
270295
private imageAssetPublishingRoleArn?: string;
271296
private lookupRoleArn?: string;
297+
private useLookupRoleForStackOperations: boolean;
272298
private qualifier?: string;
273299
private bucketPrefix?: string;
274300
private dockerTagPrefix?: string;
@@ -279,6 +305,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer {
279305

280306
constructor(private readonly props: DefaultStackSynthesizerProps = {}) {
281307
super();
308+
this.useLookupRoleForStackOperations = props.useLookupRoleForStackOperations ?? true;
282309

283310
for (const key in props) {
284311
if (props.hasOwnProperty(key)) {
@@ -453,6 +480,12 @@ export class DefaultStackSynthesizer extends StackSynthesizer {
453480
requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION,
454481
bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter,
455482
additionalDependencies: [artifactId],
483+
lookupRole: this.useLookupRoleForStackOperations && this.lookupRoleArn ? {
484+
arn: this.lookupRoleArn,
485+
assumeRoleExternalId: this.props.lookupRoleExternalId,
486+
requiresBootstrapStackVersion: MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION,
487+
bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter,
488+
} : undefined,
456489
});
457490
}
458491

packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
12
import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets';
23
import { ISynthesisSession } from '../construct-compat';
34
import { Stack } from '../stack';
@@ -100,6 +101,13 @@ export interface SynthesizeStackArtifactOptions {
100101
*/
101102
readonly cloudFormationExecutionRoleArn?: string;
102103

104+
/**
105+
* The role to use to look up values from the target AWS account
106+
*
107+
* @default - None
108+
*/
109+
readonly lookupRole?: cxschema.BootstrapRole;
110+
103111
/**
104112
* If the stack template has already been included in the asset manifest, its asset URL
105113
*

packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts

+8
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ export class CloudFormationStackArtifact extends CloudArtifact {
7575
*/
7676
public readonly cloudFormationExecutionRoleArn?: string;
7777

78+
/**
79+
* The role to use to look up values from the target AWS account
80+
*
81+
* @default - No role is assumed (current credentials are used)
82+
*/
83+
public readonly lookupRole?: cxschema.BootstrapRole;
84+
7885
/**
7986
* If the stack template has already been included in the asset manifest, its asset URL
8087
*
@@ -135,6 +142,7 @@ export class CloudFormationStackArtifact extends CloudArtifact {
135142
this.bootstrapStackVersionSsmParameter = properties.bootstrapStackVersionSsmParameter;
136143
this.terminationProtection = properties.terminationProtection;
137144
this.validateOnSynth = properties.validateOnSynth;
145+
this.lookupRole = properties.lookupRole;
138146

139147
this.stackName = properties.stackName || artifactId;
140148
this.assets = this.findMetadataByType(cxschema.ArtifactMetadataEntryType.ASSET).map(e => e.data as cxschema.AssetMetadataEntry);

packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts

+35-4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ export interface SdkHttpOptions {
7777
const CACHED_ACCOUNT = Symbol('cached_account');
7878
const CACHED_DEFAULT_CREDENTIALS = Symbol('cached_default_credentials');
7979

80+
/**
81+
* SDK configuration for a given environment
82+
* 'forEnvironment' will attempt to assume a role and if it
83+
* is not successful, then it will either:
84+
* 1. Check to see if the default credentials (local credentials the CLI was executed with)
85+
* are for the given environment. If they are then return those.
86+
* 2. If the default credentials are not for the given environment then
87+
* throw an error
88+
*
89+
* 'didAssumeRole' allows callers to whether they are receiving the assume role
90+
* credentials or the default credentials.
91+
*/
92+
export interface SdkForEnvironment {
93+
/**
94+
* The SDK for the given environment
95+
*/
96+
readonly sdk: ISDK;
97+
98+
/**
99+
* Whether or not the assume role was successful.
100+
* If the assume role was not successful (false)
101+
* then that means that the 'sdk' returned contains
102+
* the default credentials (not the assume role credentials)
103+
*/
104+
readonly didAssumeRole: boolean;
105+
}
106+
80107
/**
81108
* Creates instances of the AWS SDK appropriate for a given account/region.
82109
*
@@ -140,7 +167,11 @@ export class SdkProvider {
140167
*
141168
* The `environment` parameter is resolved first (see `resolveEnvironment()`).
142169
*/
143-
public async forEnvironment(environment: cxapi.Environment, mode: Mode, options?: CredentialsOptions): Promise<ISDK> {
170+
public async forEnvironment(
171+
environment: cxapi.Environment,
172+
mode: Mode,
173+
options?: CredentialsOptions,
174+
): Promise<SdkForEnvironment> {
144175
const env = await this.resolveEnvironment(environment);
145176
const baseCreds = await this.obtainBaseCredentials(env.account, mode);
146177

@@ -151,7 +182,7 @@ export class SdkProvider {
151182
// account.
152183
if (options?.assumeRoleArn === undefined) {
153184
if (baseCreds.source === 'incorrectDefault') { throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); }
154-
return new SDK(baseCreds.credentials, env.region, this.sdkOptions);
185+
return { sdk: new SDK(baseCreds.credentials, env.region, this.sdkOptions), didAssumeRole: false };
155186
}
156187

157188
// We will proceed to AssumeRole using whatever we've been given.
@@ -161,7 +192,7 @@ export class SdkProvider {
161192
// we can determine whether the AssumeRole call succeeds or not.
162193
try {
163194
await sdk.forceCredentialRetrieval();
164-
return sdk;
195+
return { sdk, didAssumeRole: true };
165196
} catch (e) {
166197
// AssumeRole failed. Proceed and warn *if and only if* the baseCredentials were already for the right account
167198
// or returned from a plugin. This is to cover some current setups for people using plugins or preferring to
@@ -170,7 +201,7 @@ export class SdkProvider {
170201
if (baseCreds.source === 'correctDefault' || baseCreds.source === 'plugin') {
171202
debug(e.message);
172203
warning(`${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`);
173-
return new SDK(baseCreds.credentials, env.region, this.sdkOptions);
204+
return { sdk: new SDK(baseCreds.credentials, env.region, this.sdkOptions), didAssumeRole: false };
174205
}
175206

176207
throw e;

packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class BootstrapStack {
2727
toolkitStackName = toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME;
2828

2929
const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment);
30-
const sdk = await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting);
30+
const sdk = (await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting)).sdk;
3131

3232
const currentToolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, sdk, toolkitStackName);
3333

0 commit comments

Comments
 (0)