Skip to content
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