skip to content

You define an AWS CDK stack without passing the `env` property. Where does it deploy, what do `stack.account` and `stack.region` resolve to, and what stops working?

level: middleimportance: must knowfreq 68%

answer

  1. portable template, unknown target
  2. pseudo-parameters instead of literals
  3. tokens you cannot branch on
  4. lookups need a concrete environment
  5. only two availability zones

basics

~20 s

An environment-agnostic stack deploys wherever the CDK CLI's credentials point. Its account and region become unresolved tokens backed by CloudFormation pseudo-parameters, so synth-time lookups such as Vpc.fromLookup fail and the stack assumes only two availability zones.

solid answer

~40 s

Omitting `env` makes the stack **environment-agnostic**: the template refers to its account and region through the CloudFormation pseudo-parameters `AWS::AccountId` and `AWS::Region`, so the same template can be deployed into any account or region — whichever the CLI credentials and profile resolve to at deploy time. The price is that at synthesis time the CDK does not know where the stack will land. `stack.account` and `stack.region` are unresolved tokens, so you cannot branch on them in code; every context lookup (`ec2.Vpc.fromLookup`, `route53.HostedZone.fromLookup`, `ec2.MachineImage.lookup`, `ssm.StringParameter.valueFromLookup`) fails because it needs a concrete account and region to query; and `stack.availabilityZones` falls back to just two AZs. For anything real I pin `env` explicitly per environment, so synthesis is deterministic and a wrong profile cannot silently deploy prod into a sandbox account.

code

typescript · 20 lines
typescript
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

class NetStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // Fails at synth unless this stack has a concrete account and region.
    ec2.Vpc.fromLookup(this, 'Existing', { vpcId: 'vpc-0abc123' });
  }
}

const app = new cdk.App();
new NetStack(app, 'Agnostic');                     // no env: lookup throws
new NetStack(app, 'Pinned', {                      // deterministic target
  env: { account: '111122223333', region: 'eu-west-1' },
});
new NetStack(app, 'Ambient', {                     // whatever the profile is
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

go deeper

for a junior

Know that env names the target account and region for a stack, and that leaving it out means the stack deploys wherever your CLI credentials point.

for a middle

Explain that an agnostic stack emits AWS::AccountId and AWS::Region pseudo-parameters, that account and region are unresolved tokens in code, and that lookups therefore cannot run.

for a senior

Show the operational consequences: non-deterministic synth from ambient profiles, a wrong-profile deploy landing in the wrong account, and a silent drop to two AZs changing a VPC's shape.

for a principal

Own the convention for the whole estate — where environment definitions live, how they reach every stack, and why one accidental agnostic stack undermines diff review and promotion between environments.

## What `env` actually sets `env` is a property of `StackProps` (and of `StageProps`): `{ account, region }`. It tells the CDK, at synthesis time, which AWS environment this stack is destined for. It is not credentials — it does not authenticate anything. It is a declaration that lets the CDK resolve environment-specific values while building the template, and lets the CLI refuse to deploy into the wrong place. ```typescript new ServiceStack(app, 'Prod', { env: { account: '111122223333', region: 'eu-west-1' } }); ``` ## Environment-agnostic: what you get Leave `env` out and the stack is environment-agnostic. Wherever the template needs the account or region, the CDK emits the CloudFormation pseudo-parameters `AWS::AccountId` and `AWS::Region`, which CloudFormation fills in at deploy time. The upside is genuine portability: one template, any account, any region — useful for a stack you publish for other people to deploy. The deployment target is then whatever the CLI resolves from the usual credential chain: `AWS_PROFILE`, environment variables, SSO cache, instance role. Nothing in the code constrains it. ## What stops working Everything that requires knowing the environment *before* the template exists: **Context lookups.** `ec2.Vpc.fromLookup`, `route53.HostedZone.fromLookup`, `ec2.MachineImage.lookup` and `ssm.StringParameter.valueFromLookup` all run during synthesis: the CLI calls AWS with your credentials and bakes the answer into the template as a literal. That requires a concrete account and region — both to make the call and to key the cache entry in `cdk.context.json`. In an environment-agnostic stack these throw an error telling you to specify `env`. **Branching on the environment in code.** `stack.region` is an unresolved token, a placeholder string that only becomes real inside CloudFormation. So `if (stack.region === 'us-east-1')` is always false, and string-concatenating it into a name that must be known at synth time produces a token instead of the value you expected. `cdk.Token.isUnresolved(value)` is how you check whether a value is one of these placeholders. **Availability zones.** `stack.availabilityZones` cannot ask AWS how many AZs the target region has, so an environment-agnostic stack falls back to two AZs. A VPC construct that would have spread across three subnets quietly builds two. ## The three ways to set env 1. **Omit it** — portable template, no lookups, two AZs. Fine for a sample or a redistributable stack. 2. **Hardcode it** — `env: { account: '111122223333', region: 'eu-west-1' }`. Synthesis is deterministic: the same code produces the same template on every machine and in CI, and the CLI errors if your credentials resolve to a different account. This is what production estates do, usually with the values coming from a per-environment config object in the repo. 3. **Take it from the ambient profile** — `env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }`. The CLI sets those two variables from the credentials in effect, so the stack gets a concrete environment (lookups work) but *which* environment depends on whoever ran synth. Convenient for a personal sandbox, dangerous as a default for shared environments: the same commit produces different templates for different people, and reviewing a diff tells you less than you think. ## Choosing The deciding question is whether the template is meant to be identical everywhere or specific to one place. A production stack is specific: you want the AMI, the VPC id and the AZ list pinned, you want `cdk diff` to be honest, and you want a misconfigured profile to fail loudly rather than deploy your production stack into a sandbox. Pin `env`. A library-style stack that other teams deploy into their own accounts is the genuine case for environment-agnostic — and it must then avoid lookups by design, taking VPC ids and similar values as explicit props or CloudFormation parameters instead. A useful middle pattern is to keep the environment values in one config file keyed by environment name, select it from a context value or an environment variable at the top of `bin/app.ts`, and pass the result into every stack. Environments are then explicit and greppable, and no stack is left agnostic by accident.

  • Why can't `ec2.Vpc.fromLookup` work in an environment-agnostic stack?
    Because it is a synthesis-time call, not a deploy-time one: the CLI queries EC2 with your credentials and writes the answer into the template as a literal id. That needs a concrete account and region both to make the API call and to key the cached entry in `cdk.context.json`, so the CDK errors out instead of guessing.
  • What is the risk of setting env from CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION?
    Synthesis stops being reproducible. The template depends on whichever profile happened to be active, so two engineers and CI can produce three different templates from one commit, and an operator with the wrong profile exported points a production-named stack at a sandbox. It is fine for personal sandboxes, not for shared environments.
  • Why does an environment-agnostic stack assume only two availability zones?
    `stack.availabilityZones` normally comes from a context lookup against the target region. With no environment it cannot ask, so the CDK falls back to a fixed two-AZ assumption using `Fn::GetAZs`. A VPC construct then builds two subnets per group instead of three, which is easy to miss until you compare with a pinned environment.
  • How do you tell whether a value in CDK code is a real string or an unresolved token?
    `cdk.Token.isUnresolved(value)` returns true for placeholder values that only resolve inside CloudFormation. It is the guard to use before doing anything at synthesis time — comparisons, substring work, naming decisions — with values such as `stack.account`, `stack.region` or a resource ARN.

saying these in an interview costs you the question

  • Thinks env supplies the credentials used to deploy
  • Believes stack.region can be compared in an if statement
  • Says an agnostic stack still resolves lookups at deploy time
  • Assumes omitting env means the default region from the profile is baked in
  • Never mentions the silent two-AZ fallback

context