Repository: pulumi/pulumi-cdk
Pull request #245 (2024-11-22 17:52:22)

Add ability to specify resource options at the stack level
It has been possible to specify Pulumi resource options at the stack level, but it did not flow through to the actual resources. This PR makes sure that the inheritance works correctly.

This PR also adds functionality to automatically set the Stack environment based on the App provider. Because the App creates the stacks in an async context, we can use provider functions to lookup the environment and then pass the resolved environment to the stack. 

For now this is something that the user will have to specify (example in the `lookups` example test). This would mean that all Stacks have their environment provided by default. This will cut down on the number of Intrinsics used in the generated template. The reason I am not turning this on by default is because it requires lookups to be enabled (environment agnostic stacks use Intrinsics to find availability zones, but explicit environment stacks use lookups).

If the user provides a provider to the Stack we are no longer in an async context which means we can't determine the environment from the provider and fall back to an environment agnostic stack.

re #61, re #219

corymhall: This change is part of the following stack:

- #245 ◀
    - #248

<sub>Change managed by [git-spice](https://abhinav.github.io/git-spice/).</sub>
<!-- gs:navigation comment -->


Patch:
diff --git a/README.md b/README.md
index 81a0321..81be50d 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,10 @@ combination with CDK `fromXXX` methods.
 
 **Example**
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as aws from '@pulumi/aws';
+import * as aws_route53 from 'aws-cdk-lib/aws-route53';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
     const stack = new pulumicdk.Stack('example-stack');
 
@@ -183,6 +187,9 @@ outputs. You can do this in one of two ways.
 Any `CfnOutput` that you create automatically gets added to the `App outputs`.
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as s3 from 'aws-cdk-lib/aws-s3';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
     const stack = new pulumicdk.Stack('example-stack');
     const bucket = new s3.Bucket(stack, 'Bucket');
@@ -196,6 +203,9 @@ export const bucketName = app.outputs['BucketName'];
 **AppOutputs**
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as s3 from 'aws-cdk-lib/aws-s3';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
     const stack = new pulumicdk.Stack('example-stack');
     const bucket = new s3.Bucket(stack, 'Bucket');
@@ -248,6 +258,93 @@ const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
 });
 ```
 
+### Stack Level Providers
+
+It is also possible to customize the Providers at the Stack level. This can be
+useful in cases where you need to deploy resources to different AWS regions.
+
+```ts
+import * as aws from '@pulumi/aws';
+import * as ccapi from '@pulumi/aws-native';
+import * as pulumicdk from '@pulumi/cdk';
+
+const awsProvider = new aws.Provider('aws-provider');
+const awsCCAPIProvider = new ccapi.Provider('ccapi-provider', {
+    // enable autoNaming
+    autoNaming: {
+        autoTrim: true,
+        randomSuffixMinLength: 7,
+    }
+});
+
+const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
+    // inherits the provider from the app
+    const defaultProviderStack = new pulumicdk.Stack('default-provider-stack');
+    const bucket = new s3.Bucket(defaultProviderStack, 'Bucket');
+
+    // use a different provider for this stack
+    const east2Stack = new pulumicdk.Stack('east2-stack', {
+        providers: [
+            new aws.Provider('east2-provider', { region: 'us-east-2' }),
+            new ccapi.Provider('east2-ccapi-provider', {
+                region: 'us-east-2',
+                autoNaming: {
+                    autoTrim: true,
+                    randomSuffixMinLength: 7,
+                },
+            }),
+        ],
+    });
+    const bucket2 = new s3.Bucket(east2Stack, 'Bucket');
+}, {
+    providers: [
+      dockerBuildProvider,
+      awsProvider,
+      awsCCAPIProvider,
+    ]
+});
+```
+
+One thing to note is that when you pass different custom providers to a Stack,
+by default the Stack becomes an [environment agnostic stack](https://docs.aws.amazon.com/cdk/v2/guide/configure-env.html#configure-env-examples).
+If you want to have the environment specified at the CDK Stack level, then you
+also need to provide the environment to the Stack Props, as follows:
+
+```ts
+import * as aws from '@pulumi/aws';
+import * as ccapi from '@pulumi/aws-native';
+import * as pulumicdk from '@pulumi/cdk';
+
+const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
+    // inherits the provider from the app and has the CDK env auto populated
+    // based on the default provider
+    const defaultProviderStack = new pulumicdk.Stack('default-provider-stack');
+    const bucket = new s3.Bucket(defaultProviderStack, 'Bucket');
+
+    // use a different provider for this stack
+    const east2Stack = new pulumicdk.Stack('east2-stack', {
+        props: {
+            env: {
+                region: 'us-east-2',
+                account: '12345678912',
+            },
+        },
+        providers: [
+            new aws.Provider('east2-provider', { region: 'us-east-2' }),
+            new ccapi.Provider('east2-ccapi-provider', {
+                region: 'us-east-2',
+                autoNaming: {
+                    autoTrim: true,
+                    randomSuffixMinLength: 7,
+                },
+            }),
+        ],
+    });
+    const bucket2 = new s3.Bucket(east2Stack, 'Bucket');
+});
+
+```
+
 ## CDK Lookups
 
 CDK [lookups](https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_methods) are currently disabled by default.
@@ -260,6 +357,9 @@ Instead of using CDK Lookups you can use Pulumi functions along with CDK
 
 **Example**
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as aws from '@pulumi/aws';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
     const stack = new pulumicdk.Stack('example-stack');
     // use getAmiOutput to lookup the AMI instead of ec2.LookupMachineImage
@@ -295,6 +395,9 @@ Pulumi twice (the first execution will fail).
 
 **Example**
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as aws_route53 from 'aws-cdk-lib/aws-route53';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
     const stack = new pulumicdk.Stack('example-stack');
     const hostedZone = aws_route53.HostedZone.fromLookup(this, 'hosted-zone', {
@@ -335,6 +438,7 @@ of CDK.
 Below is an example output using Pulumi's [Compliance Ready Policies](https://www.pulumi.com/docs/iac/packages-and-automation/crossguard/compliance-ready-policies/)
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
 import * as s3 from 'aws-cdk-lib/aws-s3';
 
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
@@ -358,6 +462,7 @@ Policies:
 Pulumi CDK supports CDK Aspects, including aspects like [cdk-nag](https://github.com/cdklabs/cdk-nag)
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
 import * as s3 from 'aws-cdk-lib/aws-s3';
 import { AwsSolutionsChecks } from 'cdk-nag';
 
@@ -380,6 +485,7 @@ const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutput
 Pulumi CDK also supports [CDK Policy Validation Plugins](https://docs.aws.amazon.com/cdk/v2/guide/policy-validation-synthesis.html).
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
 import { CfnGuardValidator } from '@cdklabs/cdk-validator-cfnguard';
 import * as s3 from 'aws-cdk-lib/aws-s3';
 
@@ -450,6 +556,9 @@ a reference.
 ### Simple mapping
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as aws from '@pulumi/aws';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
     const stack = new pulumicdk.Stack('example-stack');
 }, {
@@ -479,6 +588,9 @@ resources. In these cases you should return the `logicalId` of the resource
 along with the resource itself.
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as aws from '@pulumi/aws';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
     const stack = new pulumicdk.Stack('example-stack');
 }, {
@@ -516,6 +628,12 @@ you create the asset. This is because Pulumi CDK will automatically create a ECR
 Repository per image asset.
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as aws from '@pulumi/aws';
+import * as ec2 from 'aws-cdk-lib/aws-ec2';
+import * as ecs from 'aws-cdk-lib/aws-ecs';
+import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
     const stack = new pulumicdk.Stack('example-stack');
 
@@ -548,6 +666,8 @@ For example, if you wanted to set `protect` on database resources you could use
 a transform like this.
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
     const stack = new pulumicdk.Stack('example-stack');
 }, {
@@ -577,13 +697,16 @@ In order to customize the settings, you can pass in a `PulumiSynthesizer` that
 you create.
 
 ```ts
+import * as pulumicdk from '@pulumi/cdk';
+import * as s3 from 'aws-cdk-lib/aws-s3';
+
 const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
     const stack = new pulumicdk.Stack('example-stack');
     const bucket = new s3.Bucket(stack, 'Bucket');
 }, {
   appOptions: {
     props: {
-      defaultStackSynthesizer: new PulumiSynthesizer({
+      defaultStackSynthesizer: new pulumicdk.PulumiSynthesizer({
         appId: `cdk-${pulumi.getStack()}`,
         autoDeleteStagingAssets: false,
       })
@@ -624,7 +747,10 @@ default. If you _are_ configuring your own `aws-native` provider then you will
 have to enable this.
 
 ```ts
-const nativeProvider = new aws_native.Provider('cdk-native-provider', {
+import * as pulumicdk from '@pulumi/cdk';
+import * as ccapi from '@pulumi/aws-native';
+
+const nativeProvider = new ccapi.Provider('cdk-native-provider', {
   region: 'us-east-2',
   autoNaming: {
     autoTrim: true,
diff --git a/api-docs/README.md b/api-docs/README.md
index 7816a53..36af97f 100644
--- a/api-docs/README.md
+++ b/api-docs/README.md
@@ -74,14 +74,45 @@ export const bucket = app.outputs['bucket'];
 
 ###### Defined in
 
-[stack.ts:96](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L96)
+[stack.ts:105](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L105)
 
 #### Properties
 
 | Property | Modifier | Type | Default value | Description | Defined in |
 | ------ | ------ | ------ | ------ | ------ | ------ |
-| `name` | `readonly` | `string` | `undefined` | The name of the component | [stack.ts:55](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L55) |
-| `outputs` | `public` | `object` | `{}` | The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. | [stack.ts:61](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L61) |
+| `name` | `readonly` | `string` | `undefined` | The name of the component | [stack.ts:57](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L57) |
+| `outputs` | `public` | `object` | `{}` | The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. | [stack.ts:63](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L63) |
+
+#### Accessors
+
+##### env
+
+###### Get Signature
+
+> **get** **env**(): `Environment`
+
+This can be used to get the CDK Environment based on the Pulumi Provider used for the App.
+You can then use this to configure an explicit environment on Stacks.
+
+###### Example
+
+```ts
+const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
+    const stack = new pulumicdk.Stack(scope, 'pulumi-stack', {
+        props: { env: app.env },
+    });
+});
+```
+
+###### Returns
+
+`Environment`
+
+the CDK Environment configured for the App
+
+###### Defined in
+
+[stack.ts:155](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L155)
 
 ***
 
@@ -121,7 +152,7 @@ Create and register an AWS CDK stack deployed with Pulumi.
 
 ###### Defined in
 
-[stack.ts:264](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L264)
+[stack.ts:336](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L336)
 
 #### Methods
 
@@ -151,7 +182,7 @@ A Pulumi Output value.
 
 ###### Defined in
 
-[stack.ts:277](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L277)
+[stack.ts:418](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L418)
 
 ## Interfaces
 
@@ -233,6 +264,31 @@ Options specific to the Pulumi CDK App component.
 
 Options for creating a Pulumi CDK Stack
 
+Any Pulumi resource options provided at the Stack level will override those configured
+at the App level
+
+#### Example
+
+```ts
+new App('testapp', (scope: App) => {
+    // This stack will inherit the options from the App
+    new Stack(scope, 'teststack1');
+
+   // Override the options for this stack
+   new Stack(scope, 'teststack', {
+       providers: [
+         new native.Provider('custom-provider', { region: 'us-east-1' }),
+       ],
+       props: { env: { region: 'us-east-1' } },
+   })
+}, {
+     providers: [
+         new native.Provider('app-provider', { region: 'us-west-2' }),
+     ]
+
+})
+```
+
 #### Extends
 
 - `ComponentResourceOptions`
@@ -241,7 +297,7 @@ Options for creating a Pulumi CDK Stack
 
 | Property | Type | Description | Defined in |
 | ------ | ------ | ------ | ------ |
-| `props?` | `StackProps` | The CDK Stack props | [stack.ts:230](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L230) |
+| `props?` | `StackProps` | The CDK Stack props | [stack.ts:297](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L297) |
 
 ## Type Aliases
 
@@ -255,7 +311,7 @@ Options for creating a Pulumi CDK Stack
 
 #### Defined in
 
-[stack.ts:24](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L24)
+[stack.ts:26](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L26)
 
 ## Functions
 
diff --git a/examples/lookups-enabled/index.ts b/examples/lookups-enabled/index.ts
index d4fffea..1a9c402 100644
--- a/examples/lookups-enabled/index.ts
+++ b/examples/lookups-enabled/index.ts
@@ -1,4 +1,3 @@
-import * as aws from '@pulumi/aws';
 import * as pulumi from '@pulumi/pulumi';
 import * as pulumicdk from '@pulumi/cdk';
 import * as ec2 from 'aws-cdk-lib/aws-ec2';
@@ -12,14 +11,12 @@ import {
 
 const config = new pulumi.Config();
 const zoneName = config.require('zoneName');
-const accountId = config.require('accountId');
-const region = aws.config.requireRegion();
 
 export class Ec2CdkStack extends pulumicdk.Stack {
     constructor(app: pulumicdk.App, id: string) {
         super(app, id, {
             props: {
-                env: { region, account: accountId },
+                env: app.env,
             },
         });
 
diff --git a/examples/stack-provider/Pulumi.yaml b/examples/stack-provider/Pulumi.yaml
new file mode 100644
index 0000000..924045b
--- /dev/null
+++ b/examples/stack-provider/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: pulumi-stack-provider
+runtime: nodejs
+description: A minimal TypeScript Pulumi program
diff --git a/examples/stack-provider/index.ts b/examples/stack-provider/index.ts
new file mode 100644
index 0000000..e9bf6f8
--- /dev/null
+++ b/examples/stack-provider/index.ts
@@ -0,0 +1,70 @@
+import * as logs from 'aws-cdk-lib/aws-logs';
+import * as pulumi from '@pulumi/pulumi';
+import * as pulumicdk from '@pulumi/cdk';
+import * as native from '@pulumi/aws-native';
+import { RemovalPolicy } from 'aws-cdk-lib';
+
+const config = new pulumi.Config();
+const cdkRegion = config.get('cdk-region');
+const cdkAccount = config.get('cdk-account');
+const defaultRegion = config.get('default-region');
+
+export class StackProviderStack extends pulumicdk.Stack {
+    public readonly logsRegion: pulumi.Output<string>;
+    constructor(app: pulumicdk.App, id: string, providers?: pulumi.ProviderResource[]) {
+        super(app, id, {
+            providers,
+            props:
+                cdkRegion || cdkAccount
+                    ? {
+                          env: {
+                              region: cdkRegion,
+                              account: cdkAccount,
+                          },
+                      }
+                    : undefined,
+        });
+
+        const group = new logs.LogGroup(this, 'group', {
+            retention: logs.RetentionDays.ONE_DAY,
+            removalPolicy: RemovalPolicy.DESTROY,
+        });
+
+        this.logsRegion = this.asOutput(group.logGroupArn).apply((arn) => arn.split(':')[3]);
+    }
+}
+
+const app = new pulumicdk.App(
+    'app',
+    (scope: pulumicdk.App) => {
+        const stack = new StackProviderStack(scope, 'teststack', [
+            new native.Provider('ccapi-provider', {
+                region: 'us-east-1', // a different region from the app provider
+            }),
+        ]);
+        const defaultStack = new StackProviderStack(scope, 'default-stack');
+        return {
+            east1LogsRegion: stack.logsRegion,
+            east1StackRegion: stack.asOutput(stack.region),
+            defaultLogsRegion: defaultStack.logsRegion,
+            defaultStackRegion: defaultStack.asOutput(defaultStack.region),
+        };
+    },
+    {
+        providers: defaultRegion
+            ? [
+                  new native.Provider('app-provider', {
+                      region: defaultRegion as native.Region, // a different region from the default env
+                  }),
+              ]
+            : undefined,
+    },
+);
+
+// You can (we check for this though) configure a different region on the provider
+// that the stack uses vs the region in the CDK StackProps. This tests checks that both the
+// stack region and the region the resources are deployed to are the same.
+export const east1LogsRegion = app.outputs['east1LogsRegion'];
+export const defaultLogsRegion = app.outputs['defaultLogsRegion'];
+export const east1StackRegion = app.outputs['east1StackRegion'];
+export const defaultStackRegion = app.outputs['defaultStackRegion'];
diff --git a/examples/stack-provider/package.json b/examples/stack-provider/package.json
new file mode 100644
index 0000000..902cb0e
--- /dev/null
+++ b/examples/stack-provider/package.json
@@ -0,0 +1,13 @@
+{
+    "name": "pulumi-aws-cdk",
+    "devDependencies": {
+        "@types/node": "^10.0.0"
+    },
+    "dependencies": {
+        "@pulumi/aws": "^6.0.0",
+        "@pulumi/aws-native": "^1.9.0",
+        "@pulumi/cdk": "^0.5.0",
+        "aws-cdk-lib": "2.156.0",
+        "constructs": "10.3.0"
+    }
+}
diff --git a/examples/stack-provider/tsconfig.json b/examples/stack-provider/tsconfig.json
new file mode 100644
index 0000000..2666e28
--- /dev/null
+++ b/examples/stack-provider/tsconfig.json
@@ -0,0 +1,18 @@
+{
+    "compilerOptions": {
+        "strict": true,
+        "outDir": "bin",
+        "target": "es2016",
+        "module": "commonjs",
+        "moduleResolution": "node",
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "pretty": true,
+        "noFallthroughCasesInSwitch": true,
+        "noImplicitReturns": true,
+        "forceConsistentCasingInFileNames": true
+    },
+    "files": [
+        "index.ts"
+    ]
+}
\ No newline at end of file
diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts
index 30125be..f774a66 100644
--- a/src/converters/app-converter.ts
+++ b/src/converters/app-converter.ts
@@ -99,6 +99,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
     readonly resources = new Map<string, Mapping<pulumi.Resource>>();
     readonly constructs = new Map<ConstructInfo, pulumi.Resource>();
     private readonly cdkStack: cdk.Stack;
+    private readonly stackOptions?: pulumi.ComponentResourceOptions;
 
     private _stackResource?: CdkConstruct;
     private readonly graph: Graph;
@@ -113,6 +114,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
     constructor(host: AppComponent, readonly stack: StackManifest) {
         super(host);
         this.cdkStack = host.stacks[stack.id];
+        this.stackOptions = host.stackOptions[stack.id];
         this.graph = GraphBuilder.build(this.stack);
     }
 
@@ -125,6 +127,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
         for (const n of this.graph.nodes) {
             if (n.construct.id === this.stack.id) {
                 this._stackResource = new CdkConstruct(`${this.app.name}/${n.construct.path}`, n.construct.id, {
+                    ...this.stackOptions,
                     parent: this.app.component,
                     // NOTE: Currently we make the stack depend on all the assets and then all resources
                     // have the parent as the stack. This means we deploy all assets before we deploy any resources
@@ -664,18 +667,18 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
     }
 
     getAccountId(): intrinsics.Result<string> {
-        return getAccountId({ parent: this.app.component }).then((r) => r.accountId);
+        return getAccountId({ parent: this.stackResource }).then((r) => r.accountId);
     }
 
     getRegion(): intrinsics.Result<string> {
-        return getRegion({ parent: this.app.component }).then((r) => r.region);
+        return getRegion({ parent: this.stackResource }).then((r) => r.region);
     }
 
     getPartition(): intrinsics.Result<string> {
-        return getPartition({ parent: this.app.component }).then((p) => p.partition);
+        return getPartition({ parent: this.stackResource }).then((p) => p.partition);
     }
 
     getURLSuffix(): intrinsics.Result<string> {
-        return getUrlSuffix({ parent: this.app.component }).then((r) => r.urlSuffix);
+        return getUrlSuffix({ parent: this.stackResource }).then((r) => r.urlSuffix);
     }
 }
diff --git a/src/converters/artifact-converter.ts b/src/converters/artifact-converter.ts
index 1415f9e..a5578a3 100644
--- a/src/converters/artifact-converter.ts
+++ b/src/converters/artifact-converter.ts
@@ -1,5 +1,3 @@
-import * as cx from 'aws-cdk-lib/cx-api';
-import { getAccountId, getPartition, getRegion } from '@pulumi/aws-native';
 import { AppComponent } from '../types';
 
 /**
@@ -7,27 +5,4 @@ import { AppComponent } from '../types';
  */
 export abstract class ArtifactConverter {
     constructor(protected readonly app: AppComponent) {}
-
-    /**
-     * Takes a string and resolves any CDK environment placeholders (e.g. accountId, region, partition)
-     *
-     * @param s - The string that contains the placeholders to replace
-     * @returns The string with the placeholders fully resolved
-     */
-    protected resolvePlaceholders(s: string): Promise<string> {
-        const host = this.app;
-        return cx.EnvironmentPlaceholders.replaceAsync(s, {
-            async region(): Promise<string> {
-                return getRegion({ parent: host.component }).then((r) => r.region);
-            },
-
-            async accountId(): Promise<string> {
-                return getAccountId({ parent: host.component }).then((r) => r.accountId);
-            },
-
-            async partition(): Promise<string> {
-                return getPartition({ parent: host.component }).then((p) => p.partition);
-            },
-        });
-    }
 }
diff --git a/src/stack.ts b/src/stack.ts
index c9c21c6..a4ade10 100644
--- a/src/stack.ts
+++ b/src/stack.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 import * as cdk from 'aws-cdk-lib/core';
 import * as pulumi from '@pulumi/pulumi';
+import * as aws from '@pulumi/aws';
 import { AppComponent, AppOptions, AppResourceOptions } from './types';
 import { AppConverter, StackConverter } from './converters/app-converter';
 import { PulumiSynthesizer, PulumiSynthesizerBase } from './synthesizer';
@@ -20,6 +21,7 @@ import { AwsCdkCli, ICloudAssemblyDirectoryProducer } from '@aws-cdk/cli-lib-alp
 import { CdkConstruct } from './interop';
 import { makeUniqueId } from './cdk-logical-id';
 import * as native from '@pulumi/aws-native';
+import { warn } from '@pulumi/pulumi/log';
 
 export type AppOutputs = { [outputId: string]: pulumi.Output<any> };
 
@@ -70,6 +72,12 @@ export class App
      */
     public readonly stacks: { [artifactId: string]: cdk.Stack } = {};
 
+    /**
+     * The Pulumi ComponentResourceOptions associated with the stack
+     * @internal
+     */
+    readonly stackOptions: { [artifactId: string]: pulumi.ComponentResourceOptions } = {};
+
     /** @internal */
     public converter: Promise<AppConverter>;
 
@@ -91,6 +99,7 @@ export class App
 
     private readonly createFunc: (scope: App) => AppOutputs | void;
     private _app?: cdk.App;
+    private _env?: cdk.Environment;
     private appProps?: cdk.AppProps;
 
     constructor(id: string, createFunc: (scope: App) => void | AppOutputs, props?: AppResourceOptions) {
@@ -130,6 +139,26 @@ export class App
         this.registerOutputs(this.outputs);
     }
 
+    /**
+     * This can be used to get the CDK Environment based on the Pulumi Provider used for the App.
+     * You can then use this to configure an explicit environment on Stacks.
+     *
+     * @example
+     * const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
+     *     const stack = new pulumicdk.Stack(scope, 'pulumi-stack', {
+     *         props: { env: app.env },
+     *     });
+     * });
+     *
+     * @returns the CDK Environment configured for the App
+     */
+    public get env(): cdk.Environment {
+        if (!this._env) {
+            throw new Error('cdk.Environment has not been created yet');
+        }
+        return this._env;
+    }
+
     /**
      * @internal
      */
@@ -137,7 +166,7 @@ export class App
         if (!this._app) {
             throw new Error('cdk.App has not been created yet');
         }
-        return this._app!;
+        return this._app;
     }
 
     protected async initialize(props: {
@@ -150,6 +179,19 @@ export class App
         this.appOptions = props.args;
         const lookupsEnabled = process.env.PULUMI_CDK_EXPERIMENTAL_LOOKUPS === 'true';
         const lookups = lookupsEnabled && pulumi.runtime.isDryRun();
+        const [account, region] = await Promise.all([
+            native
+                .getAccountId({
+                    parent: this,
+                    ...props.opts,
+                })
+                .then((account) => account.accountId),
+            native.getRegion({ parent: this, ...props.opts }).then((region) => region.region),
+        ]);
+        this._env = {
+            account,
+            region,
+        };
         try {
             // TODO: support lookups https://github.com/pulumi/pulumi-cdk/issues/184
             await cli.synth({ quiet: true, lookups });
@@ -211,6 +253,9 @@ export class App
         app.node.children.forEach((child) => {
             if (Stack.isPulumiStack(child)) {
                 this.stacks[child.artifactId] = child;
+                if (child.options) {
+                    this.stackOptions[child.artifactId] = child.options;
+                }
             }
         });
 
@@ -222,6 +267,28 @@ export class App
 
 /**
  * Options for creating a Pulumi CDK Stack
+ *
+ * Any Pulumi resource options provided at the Stack level will override those configured
+ * at the App level
+ *
+ * @example
+ * new App('testapp', (scope: App) => {
+ *     // This stack will inherit the options from the App
+ *     new Stack(scope, 'teststack1');
+ *
+ *    // Override the options for this stack
+ *    new Stack(scope, 'teststack', {
+ *        providers: [
+ *          new native.Provider('custom-provider', { region: 'us-east-1' }),
+ *        ],
+ *        props: { env: { region: 'us-east-1' } },
+ *    })
+ * }, {
+ *      providers: [
+ *          new native.Provider('app-provider', { region: 'us-west-2' }),
+ *      ]
+ *
+ * })
  */
 export interface StackOptions extends pulumi.ComponentResourceOptions {
     /**
@@ -255,17 +322,91 @@ export class Stack extends cdk.Stack {
      */
     public converter: Promise<StackConverter>;
 
+    /**
+     * @internal
+     */
+    public options?: pulumi.ComponentResourceOptions;
+
     /**
      * Create and register an AWS CDK stack deployed with Pulumi.
      *
      * @param name The _unique_ name of the resource.
      * @param options A bag of options that control this resource's behavior.
      */
-    constructor(app: App, name: string, options?: StackOptions) {
+    constructor(private readonly app: App, name: string, options?: StackOptions) {
         super(app.app, name, options?.props);
         Object.defineProperty(this, STACK_SYMBOL, { value: true });
 
+        this.options = options;
         this.converter = app.converter.then((converter) => converter.stacks.get(this.artifactId)!);
+
+        this.validateEnv();
+    }
+
+    /**
+     * This function validates that the user has correctly configured the stack environment. There are two
+     * ways that the environment comes into play in a Pulumi CDK application. When resources are created
+     * they are created with a specific provider that is either inherited from the Stack or the App. There
+     * are some values though that CDK generates based on what environment is passed to the StackProps.
+     *
+     * Below is an example of something a user could configure (by mistake).
+     *
+     * @example
+     * new App('testapp', (scope: App) => {
+     *    new Stack(scope, 'teststack', {
+     *        providers: [
+     *          new native.Provider('native-provider', { region: 'us-east-1' }),
+     *        ],
+     *        props: { env: { region: 'us-east-2' }},
+     *    })
+     * }, {
+     *      providers: [
+     *          new native.Provider('native-provider', { region: 'us-west-2' }),
+     *      ]
+     *
+     * })
+     */
+    private validateEnv(): void {
+        const providers = providersToArray(this.options?.providers);
+        const nativeProvider = providers.find((p) => native.Provider.isInstance(p));
+        const awsProvider = providers.find((p) => aws.Provider.isInstance(p));
+
+        const awsRegion = aws.getRegionOutput({}, { parent: this.app, provider: awsProvider }).name;
+        const awsAccount = aws.getCallerIdentityOutput({}, { parent: this.app, provider: awsProvider }).accountId;
+        const nativeRegion = native.getRegionOutput({ parent: this.app, provider: nativeProvider }).region;
+        const nativeAccount = native.getAccountIdOutput({ parent: this.app, provider: nativeProvider }).accountId;
+
+        pulumi
+            .all([awsRegion, awsAccount, nativeRegion, nativeAccount])
+            .apply(([awsRegion, awsAccount, nativeRegion, nativeAccount]) => {
+                // This is to ensure that the user does not pass a different region to the provider and the stack environment.
+                if (!cdk.Token.isUnresolved(this.region) && nativeRegion !== this.region) {
+                    throw new Error(
+                        `The stack '${this.node.id}' has conflicting regions between the native provider (${nativeRegion}) and the stack environment (${this.region}).\n` +
+                            'Please ensure that the stack environment region matches the region of the native provider.',
+                    );
+                }
+
+                if (!cdk.Token.isUnresolved(this.account) && this.account !== nativeAccount) {
+                    throw new Error(
+                        `The stack '${this.node.id}' has conflicting accounts between the native provider (${nativeAccount}) and the stack environment (${this.account}).\n` +
+                            'Please ensure that the stack environment account matches the account of the native provider.',
+                    );
+                }
+
+                if (nativeAccount !== awsAccount) {
+                    warn(
+                        `The stack '${this.node.id}' uses different accounts for the AWS Provider (${awsAccount}) and the AWS CCAPI Provider (${nativeAccount}). This may be a misconfiguration.`,
+                        this.app,
+                    );
+                }
+                if (nativeRegion !== awsRegion) {
+                    warn(
+                        `The stack '${this.node.id}' uses different regions for the AWS Provider (${awsRegion}) and the AWS CCAPI Provider (${nativeRegion}). This may be a misconfiguration.`,
+                        this.app,
+                    );
+                }
+            });
     }
 
     /**
@@ -336,6 +477,12 @@ function generateAppId(): string {
         .slice(-17);
 }
 
+function providersToArray(
+    providers: pulumi.ProviderResource[] | Record<string, pulumi.ProviderResource> | undefined,
+): pulumi.ProviderResource[] {
+    return providers && !Array.isArray(providers) ? Object.values(providers) : providers ?? [];
+}
+
 /**
  * If the user has not provided the aws-native provider, we will create one by default in order
  * to enable the autoNaming feature.
@@ -347,7 +494,7 @@ function createDefaultNativeProvider(
     // will throw an error
     const region = native.config.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION!;
 
-    const newProviders = providers && !Array.isArray(providers) ? Object.values(providers) : providers ?? [];
+    const newProviders = providersToArray(providers);
     if (!newProviders.find((p) => native.Provider.isInstance(p))) {
         newProviders.push(
             new native.Provider('cdk-aws-native', {
diff --git a/src/types.ts b/src/types.ts
index 977c006..acb79dd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -83,6 +83,12 @@ export interface AppComponent {
      */
     readonly stacks: { [artifactId: string]: Stack };
 
+    /**
+     * The Pulumi ComponentResourceOptions associated with the stack
+     * @internal
+     */
+    readonly stackOptions: { [artifactId: string]: pulumi.ComponentResourceOptions };
+
     /**
      * The underlying ComponentResource
      * @internal


Test patch:
diff --git a/examples/examples_nodejs_test.go b/examples/examples_nodejs_test.go
index a465f0d..c2531c0 100644
--- a/examples/examples_nodejs_test.go
+++ b/examples/examples_nodejs_test.go
@@ -236,6 +236,71 @@ func TestScalableWebhook(t *testing.T) {
 	integration.ProgramTest(t, &test)
 }
 
+func TestStackProvider(t *testing.T) {
+	// App will use default provider and one stack will use explicit provider
+	// with region=us-east-1
+	t.Run("With default env", func(t *testing.T) {
+		test := getJSBaseOptions(t).
+			With(integration.ProgramTestOptions{
+				Dir: filepath.Join(getCwd(t), "stack-provider"),
+				ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
+					east1LogsRegion := stack.Outputs["east1LogsRegion"].(string)
+					defaultLogsRegion := stack.Outputs["defaultLogsRegion"].(string)
+					east1StackRegion := stack.Outputs["east1StackRegion"].(string)
+					defaultStackRegion := stack.Outputs["defaultStackRegion"].(string)
+					assert.Equalf(t, "us-east-1", east1LogsRegion, "Expected east1LogsRegion to be us-east-1, got %s", east1LogsRegion)
+					assert.Equalf(t, "us-east-2", defaultLogsRegion, "Expected defaultLogsRegion to be us-east-2, got %s", defaultLogsRegion)
+					assert.Equalf(t, "us-east-1", east1StackRegion, "Expected east1StackRegion to be us-east-1, got %s", east1StackRegion)
+					assert.Equalf(t, "us-east-2", defaultStackRegion, "Expected defaultStackRegion to be us-east-2, got %s", defaultStackRegion)
+				},
+			})
+
+		integration.ProgramTest(t, &test)
+	})
+
+	// App will use a custom explicit provider and one stack will use explicit provider
+	// with region=us-east-1
+	t.Run("With different env", func(t *testing.T) {
+		test := getJSBaseOptions(t).
+			With(integration.ProgramTestOptions{
+				Dir: filepath.Join(getCwd(t), "stack-provider"),
+				Config: map[string]string{
+					"default-region": "us-west-2",
+				},
+				ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
+					east1LogsRegion := stack.Outputs["east1LogsRegion"].(string)
+					defaultLogsRegion := stack.Outputs["defaultLogsRegion"].(string)
+					east1StackRegion := stack.Outputs["east1StackRegion"].(string)
+					defaultStackRegion := stack.Outputs["defaultStackRegion"].(string)
+					assert.Equalf(t, "us-east-1", east1LogsRegion, "Expected east1LogsRegion to be us-east-1, got %s", east1LogsRegion)
+					assert.Equalf(t, "us-west-2", defaultLogsRegion, "Expected defaultLogsRegion to be us-west-2, got %s", defaultLogsRegion)
+					assert.Equalf(t, "us-east-1", east1StackRegion, "Expected east1StackRegion to be us-east-1, got %s", east1StackRegion)
+					assert.Equalf(t, "us-west-2", defaultStackRegion, "Expected defaultStackRegion to be us-west-2, got %s", defaultStackRegion)
+				},
+			})
+
+		integration.ProgramTest(t, &test)
+	})
+
+	t.Run("Fails with different cdk env", func(t *testing.T) {
+		var output bytes.Buffer
+		test := getJSBaseOptions(t).
+			With(integration.ProgramTestOptions{
+				Dir:           filepath.Join(getCwd(t), "stack-provider"),
+				Stderr:        &output,
+				ExpectFailure: true,
+				Config: map[string]string{
+					"cdk-region": "us-east-2",
+				},
+				ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
+					assert.Contains(t, output.String(), "The stack 'teststack' has conflicting regions between the native provider (us-east-1) and the stack environment (us-east-2)")
+				},
+			})
+
+		integration.ProgramTest(t, &test)
+	})
+}
+
 func TestTheBigFan(t *testing.T) {
 	test := getJSBaseOptions(t).
 		With(integration.ProgramTestOptions{
diff --git a/tests/basic.test.ts b/tests/basic.test.ts
index 23e57c4..c4290ae 100644
--- a/tests/basic.test.ts
+++ b/tests/basic.test.ts
@@ -16,7 +16,8 @@ import * as aws from '@pulumi/aws';
 import * as native from '@pulumi/aws-native';
 import * as s3 from 'aws-cdk-lib/aws-s3';
 import * as output from '../src/output';
-import { setMocks, testApp } from './mocks';
+import { Stack, App, AppOutputs } from '../src/stack';
+import { setMocks, testApp, awaitApp, promiseOf } from './mocks';
 import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
 import { Vpc } from 'aws-cdk-lib/aws-ec2';
 import { aws_ssm } from 'aws-cdk-lib';
@@ -214,3 +215,61 @@ describe('Basic tests', () => {
         );
     });
 });
+
+describe('Stack environment validation', () => {
+    beforeEach(() => {
+        setMocks();
+    });
+    test('app default env', async () => {
+        const app = new App('testapp', (scope: App): AppOutputs => {
+            const stack = new Stack(scope, 'teststack');
+            new s3.CfnBucket(stack, 'bucket');
+            return {
+                region: stack.asOutput(stack.region),
+            };
+        });
+        await awaitApp(app);
+        const region = await promiseOf(app.outputs['region']);
+        expect(region).toEqual('us-east-2');
+    });
+
+    test('app custom env', async () => {
+        const app = new App(
+            'testapp',
+            (scope: App): AppOutputs => {
+                const stack = new Stack(scope, 'teststack');
+                new s3.CfnBucket(stack, 'bucket');
+                return {
+                    region: stack.asOutput(stack.region),
+                };
+            },
+            {
+                providers: [new native.Provider('custom-region_us-west-2', { region: native.Region.UsWest2 })],
+            },
+        );
+        await awaitApp(app);
+        const region = await promiseOf(app.outputs['region']);
+        expect(region).toContain('us-west-2');
+    });
+
+    test('app and custom stack env', async () => {
+        const app = new App(
+            'testapp',
+            (scope: App): AppOutputs => {
+                const stack = new Stack(scope, 'teststack', {
+                    providers: [new native.Provider('custom-region_us-west-1', { region: native.Region.UsWest1 })],
+                });
+                new s3.CfnBucket(stack, 'bucket');
+                return {
+                    region: stack.asOutput(stack.region),
+                };
+            },
+            {
+                providers: [new native.Provider('custom-region_us-west-2', { region: native.Region.UsWest2 })],
+            },
+        );
+        await awaitApp(app);
+        const region = await promiseOf(app.outputs['region']);
+        expect(region).toContain('us-west-1');
+    });
+});
diff --git a/tests/cdk-resource.test.ts b/tests/cdk-resource.test.ts
index c26ec91..602298a 100644
--- a/tests/cdk-resource.test.ts
+++ b/tests/cdk-resource.test.ts
@@ -137,7 +137,7 @@ describe('CDK Construct tests', () => {
                     {
                         Action: 'events:PutEvents',
                         Effect: 'Allow',
-                        Principal: { AWS: 'arn:aws:iam::12345678912:root' },
+                        Principal: { AWS: 'arn:aws:iam::12345678910:root' },
                         Resource: 'testbus_arn',
                         Sid: 'cdk-testsid',
                     },
diff --git a/tests/mocks.ts b/tests/mocks.ts
index c97a21d..eb590d7 100644
--- a/tests/mocks.ts
+++ b/tests/mocks.ts
@@ -23,6 +23,7 @@ export class MockAppComponent extends pulumi.ComponentResource implements AppCom
     public readonly name = 'stack';
     public readonly assemblyDir: string;
     stacks: { [artifactId: string]: CdkStack } = {};
+    stackOptions: { [artifactId: string]: pulumi.ComponentResourceOptions } = {};
     dependencies: CdkConstruct[] = [];
 
     component: pulumi.ComponentResource;
@@ -35,15 +36,17 @@ export class MockAppComponent extends pulumi.ComponentResource implements AppCom
     }
 }
 
-export async function testApp(fn: (scope: Construct) => void, options?: pulumi.ComponentResourceOptions) {
+export async function testApp(
+    fn: (scope: Construct) => void,
+    options?: pulumi.ComponentResourceOptions,
+    withEnv?: boolean,
+) {
+    const env = withEnv ? { account: '12345678912', region: 'us-east-1' } : undefined;
     class TestStack extends Stack {
         constructor(app: App, id: string) {
             super(app, id, {
                 props: {
-                    env: {
-                        region: 'us-east-1',
-                        account: '12345678912',
-                    },
+                    env,
                 },
             });
 
@@ -64,6 +67,10 @@ export async function testApp(fn: (scope: Construct) => void, options?: pulumi.C
             ...options,
         },
     );
+    await awaitApp(app);
+}
+
+export async function awaitApp(app: App): Promise<void> {
     const converter = await app.converter;
     await Promise.all(
         Array.from(converter.stacks.values()).flatMap((stackConverter) => {
@@ -83,6 +90,11 @@ export function setMocks(resources?: MockResourceArgs[]) {
                         accountId: '12345678910',
                     };
                 case 'aws-native:index:getRegion':
+                    if (args.provider?.includes('custom-region')) {
+                        return {
+                            region: args.provider,
+                        };
+                    }
                     return {
                         region: 'us-east-2',
                     };
diff --git a/tests/options.test.ts b/tests/options.test.ts
index 068b67d..ee1a11f 100644
--- a/tests/options.test.ts
+++ b/tests/options.test.ts
@@ -1,3 +1,4 @@
+import * as native from '@pulumi/aws-native';
 import * as pulumi from '@pulumi/pulumi';
 import { StackManifest } from '../src/assembly';
 import { StackConverter } from '../src/converters/app-converter';
@@ -254,4 +255,64 @@ describe('options', () => {
             }),
         );
     });
+
+    test('provider can be set at stack level', async () => {
+        const manifest = new StackManifest({
+            id: 'stack',
+            templatePath: 'test/stack',
+            metadata: {
+                'stack/bucket': 'bucket',
+            },
+            tree: {
+                path: 'stack',
+                id: 'stack',
+                children: {
+                    bucket: {
+                        id: 'bucket',
+                        path: 'stack/bucket',
+                        attributes: {
+                            'aws:cdk:cloudformation:type': 'AWS::S3::Bucket',
+                        },
+                    },
+                },
+                constructInfo: {
+                    fqn: 'aws-cdk-lib.Stack',
+                    version: '2.149.0',
+                },
+            },
+            template: {
+                Resources: {
+                    bucket: {
+                        Type: 'AWS::S3::Bucket',
+                        Properties: {},
+                    },
+                },
+            },
+            dependencies: [],
+        });
+        const appComponent = new MockAppComponent('/tmp/foo/bar/does/not/exist');
+
+        appComponent.stackOptions['stack'] = {
+            providers: [
+                new native.Provider('test-native', {
+                    region: 'us-west-2',
+                }),
+            ],
+        };
+        const converter = new StackConverter(appComponent, manifest);
+        converter.convert(new Set());
+        expect(pulumi.CustomResource).toHaveBeenCalledWith(
+            'aws-native:s3:Bucket',
+            'bucket',
+            expect.anything(),
+            expect.objectContaining({
+                parent: expect.objectContaining({
+                    __name: 'stack/stack',
+                    __providers: expect.objectContaining({
+                        'aws-native': expect.objectContaining({ __name: 'test-native' }),
+                    }),
+                }),
+            }),
+        );
+    });
 });