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): diff now uses the lookup Role for new-style synthesis #18277

Merged
merged 9 commits into from
Jan 10, 2022
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@

/**
* Information needed to access an IAM role created
* as part of the bootstrap process
*/
export interface BootstrapRole {
/**
* The ARN of the IAM role created as part of bootrapping
* e.g. lookupRoleArn
*/
readonly arn: string;

/**
* External ID to use when assuming the bootstrap role
*
* @default - No external ID
*/
readonly assumeRoleExternalId?: string;

/**
* Version of bootstrap stack required to use this role
*
* @default - No bootstrap stack required
*/
readonly requiresBootstrapStackVersion?: number;

/**
* Name of SSM parameter with bootstrap stack version
*
* @default - Discover SSM parameter by reading stack
*/
readonly bootstrapStackVersionSsmParameter?: string;
}

/**
* Artifact properties for CloudFormation stacks.
*/
Expand Down Expand Up @@ -56,6 +89,13 @@ export interface AwsCloudFormationStackProperties {
*/
readonly cloudFormationExecutionRoleArn?: string;

/**
* The role to use to look up values from the target AWS account
*
* @default - No role is assumed (current credentials are used)
*/
readonly lookupRole?: BootstrapRole;

/**
* If the stack template has already been included in the asset manifest, its asset URL
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@
"description": "The role that is passed to CloudFormation to execute the change set (Default - No role is passed (currently assumed role/credentials are used))",
"type": "string"
},
"lookupRole": {
"description": "The role to use to look up values from the target AWS account (Default - No role is assumed (current credentials are used))",
"$ref": "#/definitions/BootstrapRole"
},
"stackTemplateAssetObjectUrl": {
"description": "If the stack template has already been included in the asset manifest, its asset URL (Default - Not uploaded yet, upload just before deploying)",
"type": "string"
Expand All @@ -328,6 +332,31 @@
"templateFile"
]
},
"BootstrapRole": {
"description": "Information needed to access an IAM role created\nas part of the bootstrap process",
"type": "object",
"properties": {
"arn": {
"description": "The ARN of the IAM role created as part of bootrapping\ne.g. lookupRoleArn",
"type": "string"
},
"assumeRoleExternalId": {
"description": "External ID to use when assuming the bootstrap role (Default - No external ID)",
"type": "string"
},
"requiresBootstrapStackVersion": {
"description": "Version of bootstrap stack required to use this role (Default - No bootstrap stack required)",
"type": "number"
},
"bootstrapStackVersionSsmParameter": {
"description": "Name of SSM parameter with bootstrap stack version (Default - Discover SSM parameter by reading stack)",
"type": "string"
}
},
"required": [
"arn"
]
},
"AssetManifestProperties": {
"description": "Artifact properties for the Asset Manifest",
"type": "object",
Expand Down Expand Up @@ -598,7 +627,7 @@
}
},
"returnAsymmetricSubnets": {
"description": "Whether to populate the subnetGroups field of the {@link VpcContextResponse},\nwhich contains potentially asymmetric subnet groups.",
"description": "Whether to populate the subnetGroups field of the{@linkVpcContextResponse},\nwhich contains potentially asymmetric subnet groups.",
"default": false,
"type": "boolean"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"15.0.0"}
{"version":"16.0.0"}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier';
*/
const MIN_BOOTSTRAP_STACK_VERSION = 6;

/**
* The minimum bootstrap stack version required
* to use the lookup role.
*/
const MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION = 8;

/**
* Configuration properties for DefaultStackSynthesizer
*/
Expand Down Expand Up @@ -91,6 +97,25 @@ export interface DefaultStackSynthesizerProps {
*/
readonly lookupRoleArn?: string;

/**
* External ID to use when assuming lookup role
*
* @default - No external ID
*/
readonly lookupRoleExternalId?: string;

/**
* Use the bootstrapped lookup role for (read-only) stack operations
*
* Use the lookup role when performing a `cdk diff`. If set to `false`, the
* `deploy role` credentials will be used to perform a `cdk diff`.
*
* Requires bootstrap stack version 8.
*
* @default true
*/
readonly useLookupRoleForStackOperations?: boolean;

/**
* External ID to use when assuming role for image asset publishing
*
Expand Down Expand Up @@ -269,6 +294,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer {
private fileAssetPublishingRoleArn?: string;
private imageAssetPublishingRoleArn?: string;
private lookupRoleArn?: string;
private useLookupRoleForStackOperations: boolean;
private qualifier?: string;
private bucketPrefix?: string;
private dockerTagPrefix?: string;
Expand All @@ -279,6 +305,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer {

constructor(private readonly props: DefaultStackSynthesizerProps = {}) {
super();
this.useLookupRoleForStackOperations = props.useLookupRoleForStackOperations ?? true;

for (const key in props) {
if (props.hasOwnProperty(key)) {
Expand Down Expand Up @@ -453,6 +480,12 @@ export class DefaultStackSynthesizer extends StackSynthesizer {
requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION,
bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter,
additionalDependencies: [artifactId],
lookupRole: this.useLookupRoleForStackOperations && this.lookupRoleArn ? {
arn: this.lookupRoleArn,
assumeRoleExternalId: this.props.lookupRoleExternalId,
requiresBootstrapStackVersion: MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION,
bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter,
} : undefined,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets';
import { ISynthesisSession } from '../construct-compat';
import { Stack } from '../stack';
Expand Down Expand Up @@ -100,6 +101,13 @@ export interface SynthesizeStackArtifactOptions {
*/
readonly cloudFormationExecutionRoleArn?: string;

/**
* The role to use to look up values from the target AWS account
*
* @default - None
*/
readonly lookupRole?: cxschema.BootstrapRole;

/**
* If the stack template has already been included in the asset manifest, its asset URL
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export class CloudFormationStackArtifact extends CloudArtifact {
*/
public readonly cloudFormationExecutionRoleArn?: string;

/**
* The role to use to look up values from the target AWS account
*
* @default - No role is assumed (current credentials are used)
*/
public readonly lookupRole?: cxschema.BootstrapRole;

/**
* If the stack template has already been included in the asset manifest, its asset URL
*
Expand Down Expand Up @@ -135,6 +142,7 @@ export class CloudFormationStackArtifact extends CloudArtifact {
this.bootstrapStackVersionSsmParameter = properties.bootstrapStackVersionSsmParameter;
this.terminationProtection = properties.terminationProtection;
this.validateOnSynth = properties.validateOnSynth;
this.lookupRole = properties.lookupRole;

this.stackName = properties.stackName || artifactId;
this.assets = this.findMetadataByType(cxschema.ArtifactMetadataEntryType.ASSET).map(e => e.data as cxschema.AssetMetadataEntry);
Expand Down
39 changes: 35 additions & 4 deletions packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ export interface SdkHttpOptions {
const CACHED_ACCOUNT = Symbol('cached_account');
const CACHED_DEFAULT_CREDENTIALS = Symbol('cached_default_credentials');

/**
* SDK configuration for a given environment
* 'forEnvironment' will attempt to assume a role and if it
* is not successful, then it will either:
* 1. Check to see if the default credentials (local credentials the CLI was executed with)
* are for the given environment. If they are then return those.
* 2. If the default credentials are not for the given environment then
* throw an error
*
* 'didAssumeRole' allows callers to whether they are receiving the assume role
* credentials or the default credentials.
*/
export interface SdkForEnvironment {
/**
* The SDK for the given environment
*/
readonly sdk: ISDK;

/**
* Whether or not the assume role was successful.
* If the assume role was not successful (false)
* then that means that the 'sdk' returned contains
* the default credentials (not the assume role credentials)
*/
readonly didAssumeRole: boolean;
}

/**
* Creates instances of the AWS SDK appropriate for a given account/region.
*
Expand Down Expand Up @@ -140,7 +167,11 @@ export class SdkProvider {
*
* The `environment` parameter is resolved first (see `resolveEnvironment()`).
*/
public async forEnvironment(environment: cxapi.Environment, mode: Mode, options?: CredentialsOptions): Promise<ISDK> {
public async forEnvironment(
environment: cxapi.Environment,
mode: Mode,
options?: CredentialsOptions,
): Promise<SdkForEnvironment> {
const env = await this.resolveEnvironment(environment);
const baseCreds = await this.obtainBaseCredentials(env.account, mode);

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

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

throw e;
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class BootstrapStack {
toolkitStackName = toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME;

const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment);
const sdk = await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting);
const sdk = (await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting)).sdk;

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

Expand Down
Loading