Top 40 AWS CDK Interview Questions 2026
AWS CDK questions show up in cloud engineer, platform, DevOps and backend interviews at any team that runs on AWS and has moved past hand-written CloudFormation. Solutions architects get them too, usually as "why would you pick CDK over Terraform here?" The bar is not whether you can type new s3.Bucket(...) from memory. Anyone can, and the API docs are a tab away.
What the interviewer is really testing is whether you understand that CDK is a program that generates CloudFormation, and what that implies: when a value is known, why a rename can delete a production table, why two stacks can lock each other in place, and how you test infrastructure before it touches an account. The 40 questions below escalate from fundamentals to the scenario questions live rounds actually use. All of them are about CDK v2 (aws-cdk-lib); v1 reached end of support on June 1, 2023, so if you are asked about v1 the right answer is "migrate".
Practice alongside this post. The AWS question bank drills the services CDK provisions as timed multiple-choice, and the Terraform question bank covers the state and plan concepts that every "CDK vs Terraform" question leans on. Keep the Terraform and IaC cheat sheet and the AWS CLI cheat sheet open while you read, and work through the AWS Solutions Architect track if the role is architecture-heavy. The sibling posts are Top 50 AWS interview questions, Top 50 Terraform interview questions and Top 50 DevOps interview questions.
Part 1: Fundamentals (1-8)
1. What is the AWS CDK, and what does it actually deploy?
The AWS Cloud Development Kit is an infrastructure-as-code framework where you define resources in a general-purpose language (TypeScript, JavaScript, Python, Java, C# or Go) and the CDK turns that program into AWS CloudFormation templates. The deploy itself is a CloudFormation deployment: the CDK CLI uploads assets, then creates or updates CloudFormation stacks.
That second sentence is the one interviewers listen for. CDK does not call service APIs to create your bucket; CloudFormation does. So CloudFormation's rules still apply - stack resource limits, update behaviors (no interruption, some interruption, replacement), rollback on failure, exports that cannot be deleted while imported. CDK gives you loops, functions, types, classes and package management on top. It does not give you an escape from CloudFormation semantics.
The follow-up: "So what does CDK add that CloudFormation lacks?" Abstraction (one line that creates a bucket, a policy and an encryption key), sensible secure defaults, IAM grant helpers, asset bundling, and the ability to unit test your infrastructure as code.
2. Explain L1, L2 and L3 constructs.
A construct is the basic building block: a node in a tree that represents one or more cloud resources.
| Level | What it is | Example | When you use it |
|---|
| L1 | Generated 1:1 from the CloudFormation spec, named Cfn* | s3.CfnBucket | A brand-new property or service the L2 does not cover yet |
| L2 | Hand-written, intent-based API with defaults and helpers | s3.Bucket, bucket.grantRead(fn) | Most of the time |
| L3 | Patterns that combine several resources | ecsPatterns.ApplicationLoadBalancedFargateService | Standard architectures, or your own company patterns |
L1s expose every CloudFormation property and nothing more. L2s add defaults (an L2 bucket blocks public access by default), methods like grantReadWrite, addEventNotification and metric*, and typed enums instead of strings. L3s are opinionated and fast to start with, but you inherit their opinions.
The senior follow-up: "Your platform team wants to publish an L3 for every service. Good idea?" Mostly yes - an internal L3 is where you encode tagging, encryption and logging standards once - but keep the props surface small and give consumers an escape hatch, or teams will fork it.
3. What are an App, a Stack and a Stage?
- App - the root of the construct tree. One
cdk.App per program; cdk synth runs this program.
- Stack - the unit of deployment. Each
Stack becomes one CloudFormation stack and one template. Everything inside a stack deploys and rolls back together.
- Stage - a group of stacks that are deployed together as one copy of your application, usually per environment.
new MyAppStage(app, 'Prod', { env: prodEnv }) gives you the whole app in prod; another instance gives you staging.
class MyAppStage extends cdk.Stage {
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props);
const data = new DataStack(this, 'Data');
new ApiStack(this, 'Api', { table: data.table });
}
}
const app = new cdk.App();
new MyAppStage(app, 'Staging', { env: { account: '111111111111', region: 'us-east-1' } });
new MyAppStage(app, 'Prod', { env: { account: '222222222222', region: 'us-east-1' } });
Stages matter most in CDK Pipelines, where pipeline.addStage(...) takes a Stage, not a Stack (question 32).
4. What is the difference between cdk synth and cdk deploy?
cdk synth runs your app and writes a cloud assembly to cdk.out/: one CloudFormation template per stack, an asset manifest, and a manifest.json describing stacks, dependencies and environments. It does not touch CloudFormation. It may call AWS for context lookups (question 13) if a value is not already cached.
cdk deploy synthesizes (unless you pass a pre-built assembly with --app cdk.out), publishes assets to the bootstrap bucket and repository, then creates or updates the CloudFormation stacks in dependency order, by default through a change set.
The interview point: synthesis is deterministic code execution, deployment is a remote, stateful operation. That is why "synth once, deploy the same artifact everywhere" is the CI pattern. If you re-synthesize per environment, you can deploy something different from what you tested.
5. What are tokens, and why can't I if on bucket.bucketName?
When you read an attribute that is only known at deploy time - a generated bucket name, an ARN, a VPC ID - CDK gives you a token: a placeholder that renders as something like ${Token[TOKEN.123]} in your program and resolves to Fn::GetAtt or Ref in the template.
const bucket = new s3.Bucket(this, 'Uploads');
console.log(bucket.bucketName);
if (bucket.bucketName.startsWith('prod')) { }
Your code runs at synth time; the real value exists only after CloudFormation creates the resource. So branching, string slicing or length checks on tokens are bugs. If you need a deploy-time decision, express it in CloudFormation (CfnCondition, Fn.conditionIf) or decide from something known at synth time - a context value or a prop. cdk.Token.isUnresolved(value) tells you whether you are holding a token.
6. What changed between CDK v1 and v2?
- One library. v1 shipped each service as its own package (
@aws-cdk/aws-s3, @aws-cdk/aws-lambda), and mismatched versions broke builds. v2 puts all stable constructs in aws-cdk-lib, with constructs as a separate package.
- Experimental modules are separate. Unstable L2s ship as
-alpha packages, so stable APIs follow semantic versioning.
- Modern bootstrapping is required (question 9).
- Feature flags reset. Many v1 flags became the default behavior in v2.
- Support. v1 entered maintenance June 1, 2022 and ended support June 1, 2023.
If asked how you migrated: update imports to aws-cdk-lib, re-bootstrap every environment with the modern template, run cdk diff against every stack and look specifically for resource replacements before deploying.
7. Which languages can you use, and how does that work?
TypeScript, JavaScript, Python, Java, C# and Go. The constructs are written in TypeScript and exposed to the other languages through jsii, which generates bindings. Every language therefore has the same constructs and the same behavior.
The practical follow-ups: every language still needs Node.js installed, because the CLI and the jsii runtime run on it; examples and community answers skew toward TypeScript; and in Python or Java you pay a small synth-time cost for the cross-language bridge. A reasonable answer to "which language would you pick?" is "the one the application team already uses, so they own their infrastructure", with TypeScript as the default when there is no strong reason otherwise.
8. What is a construct ID, and how does it differ from a resource's physical name?
Every construct takes (scope, id, props). The id must be unique among its siblings and becomes part of the construct's path (MyStack/Api/Handler). From the path, CDK derives the logical ID in the CloudFormation template. The physical name is the real name in AWS (my-app-uploads-a1b2c3), which CDK lets CloudFormation generate unless you set it.
Prefer generated physical names. A hard-coded bucketName or tableName means any change that requires replacement fails, because CloudFormation creates the new resource before deleting the old one and the name is already taken. It also blocks deploying the same stack twice into one account. Pass names to consumers through references or environment variables instead.
Part 2: Bootstrapping, assets and environments (9-15)
9. What is bootstrapping and what does it create?
Before CDK can deploy into an environment (an account and Region pair), you run cdk bootstrap, which deploys a CloudFormation stack named CDKToolkit by default. It contains:
- An S3 bucket for file assets (Lambda code, templates too large to inline).
- An ECR repository for Docker image assets.
- IAM roles: a deploy role the CLI assumes, file and image publishing roles, a lookup role for context queries, and the CloudFormation execution role that actually creates your resources.
- An SSM parameter recording the bootstrap version.
cdk bootstrap aws://111111111111/us-east-1
Each environment is independent and must be bootstrapped separately. Two follow-ups come up. First, the execution role's policy defaults to AdministratorAccess; in a regulated account you pass --cloudformation-execution-policies with something narrower, accepting that CDK can then only deploy what that policy allows. Second, stacks synthesized with the default synthesizer check the bootstrap version at deploy time, so an old bootstrap stack produces a clear error asking you to re-bootstrap (question 40).
10. What are assets and how are they deployed?
Assets are local files, directories or Docker images your app needs at deploy time: Lambda code, a static site build, a container image.
new lambda.Function(this, 'Handler', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')),
});
During synth, CDK fingerprints the asset and records it in the asset manifest. During deploy, file assets are zipped and uploaded to the bootstrap S3 bucket, and image assets are built and pushed to the bootstrap ECR repository, before CloudFormation runs. The template references them by content hash, so an unchanged asset is not re-uploaded and does not trigger an update.
NodejsFunction and PythonFunction add bundling (esbuild for Node, Docker-based packaging for Python) as part of synth. Gotcha: if bundling is non-deterministic - a timestamp, an unpinned dependency - the hash changes every synth and every deploy updates the function. And for CI, cdk publish-assets lets you upload assets as a separate step from deployment.
11. What is an environment-agnostic stack, and when does it bite?
A stack without env is environment-agnostic: it deploys to whatever account and Region the CLI credentials resolve to, and Stack.of(this).account / .region are tokens (AWS::AccountId, AWS::Region).
new MyStack(app, 'Agnostic');
new MyStack(app, 'Pinned', { env: { account: '111111111111', region: 'us-east-1' } });
It bites in three places. Context lookups like Vpc.fromLookup() fail, because the CLI does not know which account to query. Constructs that need a concrete Region at synth time (for example, picking Availability Zones) fall back to dummy values - an agnostic stack sees only two AZs. And code that branches on region === 'us-east-1' silently takes the wrong branch, because a token never equals a literal (question 5). Production stacks should pin env explicitly; process.env.CDK_DEFAULT_ACCOUNT is convenient for dev, risky for prod.
12. What is context and what is cdk.context.json?
Context is a set of key-value pairs available to the app at synth time. It comes from cdk.json, cdk.context.json, --context key=value on the command line, and node.setContext() in code. You read it with this.node.tryGetContext('key').
cdk.context.json is the cache for lookup results: AZs, AMI IDs, VPC details. The first Vpc.fromLookup() queries AWS and writes the answer there; every later synth reads the cached value.
Commit cdk.context.json to source control. That keeps synthesis deterministic: without it, a CI synth could pick up a new AMI or a different VPC and deploy a change nobody reviewed, and a pipeline without lookup permissions could fail outright. To refresh a value deliberately, run cdk context --reset <key> and review the diff. Feature flags live in cdk.json and are not cleared by cdk context --reset or --clear.
13. How do fromLookup and fromXxxArn differ?
Both give you a reference to a resource the app does not manage, but they work differently.
ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: 'vpc-123' }) queries the account at synth time (and caches the result) so it can return a fully-shaped object: subnets, route tables, AZs. It needs an environment-specific stack and lookup permissions.
s3.Bucket.fromBucketArn(this, 'B', arn) and fromBucketName do not call AWS. They build a lightweight proxy from what you give them.
Either way, the imported resource is not part of your stack. Changes you make to the proxy do not change the real resource; for example, addToResourcePolicy on an imported bucket does nothing, because there is no bucket policy resource in your template to add to. Interviewers like this gotcha: "I granted access with importedBucket.grantRead(role) - why does it work?" Because the grant lands on the role's policy, which you do own; the bucket side is untouched.
14. What are feature flags in CDK?
Feature flags are context keys in cdk.json (named like @aws-cdk/aws-s3:...) that opt an app into a new behavior that would otherwise be a breaking change. cdk init writes the current recommended flags into new projects, so new apps get the new behavior. Existing apps keep the old behavior until they opt in, which protects deployed resources from surprise replacement on a library upgrade.
Recent CLIs include cdk flags (currently behind --unstable=flags) to inspect flags, show the recommended value, and set unconfigured flags.
The interview follow-up: "You upgraded aws-cdk-lib and want the new defaults. How?" Enable flags one at a time, run cdk diff against every environment for each one, and read every replacement before deploying. Some flags change logical IDs or resource properties in ways that replace resources.
15. What does a removal policy do?
It sets the CloudFormation DeletionPolicy and UpdateReplacePolicy on a resource: what happens to the real resource when it is removed from the stack or replaced.
new dynamodb.Table(this, 'Orders', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
new s3.Bucket(this, 'Scratch', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
Stateful L2s like s3.Bucket and dynamodb.Table default to RETAIN, so deleting the stack orphans them. That is the safe default for production and annoying in ephemeral test stacks, where DESTROY is usual. SNAPSHOT exists for resources that support it, such as RDS instances. The follow-up: "Retain does not protect you from a replacement emptying your data." A replaced table with RETAIN keeps the old table, but your application now points at a new, empty one.
Part 3: Identity and safe change (16-22)
16. How does CDK generate logical IDs?
From the construct path. For a construct at MyStack/Api/Handler, CDK concatenates the path components and appends a hash of the full path, producing something like ApiHandler5E7490E8. Top-level resources directly in the stack keep a cleaner ID. A construct with the id Default is special: it is left out of the path, which is how an L2 such as Bucket makes its inner CfnBucket (id Resource) produce a stable ID.
Why it matters: CloudFormation identifies resources by logical ID. Same ID, it updates in place (or replaces if an immutable property changed). New ID, it creates a new resource and deletes the old one. So logical ID stability is resource stability. Moving a construct into a new parent, renaming its id, or renaming a parent all change the path, change the hash, and look like delete-plus-create to CloudFormation.
17. You renamed a construct. What happens on the next deploy?
The logical ID changes, so CloudFormation sees an old resource removed and a new one added. It creates the new resource and deletes the old one (unless the removal policy retains it). For a Lambda function, that is usually harmless. For a DynamoDB table, an RDS instance or a Cognito user pool, it is data loss or an outage. And if the resource has a hard-coded physical name, the create fails because the name is taken.
cdk diff shows it; you need to read it. Look for paired [-] and [+] lines with the same resource type, or [-/+] / "requires replacement" markers. A good team gates on this: a CI step that fails if the diff for a stateful resource type includes a replacement, and a stack policy or RETAIN on anything that holds data.
18. How do you refactor CDK code without replacing deployed resources?
Four tools, from oldest to newest:
- Use the
Default id. When you wrap an existing resource in a new construct, give the inner construct the id Default; it is skipped in the path, so the logical ID can stay the same.
overrideLogicalId. Pin the old ID on the new location:
const table = new dynamodb.Table(this, 'OrdersTable', { });
(table.node.defaultChild as dynamodb.CfnTable).overrideLogicalId('OrdersTable5E1C2B8A');
It works, but it is a permanent wart; leave a comment explaining it.
3. cdk import for the case where the resource has already left the stack (retained) and you want to adopt it back.
4. cdk refactor. The CLI compares your code with the deployed state, works out which resources moved or were renamed (including between stacks in the same environment), and uses CloudFormation's stack refactoring API to change logical IDs without replacing anything. It is in preview: you run it with --unstable=refactor, it needs a recently updated bootstrap stack, and it refuses to run if the change also adds, deletes or modifies resources - refactor in one deploy, change behavior in the next. --dry-run prints the mapping first.
19. What does cdk diff show, and what are its limits?
cdk diff compares the synthesized template with the deployed one (or a local template) and prints resource additions, removals, property changes, replacements, and a separate section for security changes: IAM statements and security group rules that broaden access.
cdk diff ApiStack --context env=prod
Its limits are the interview part. It compares templates, not real-world state, so drift made in the console is invisible to it (that is cdk drift, or CloudFormation drift detection). It cannot always know whether a property change requires replacement until CloudFormation evaluates it, which is why it can use a change set to get CloudFormation's answer. And it shows asset changes only as a hash change, not what changed in your Lambda code. In CI, make security changes explicit: cdk deploy --require-approval defaults to prompting on broadening changes, and a pipeline should surface the same diff to a reviewer.
20. What are cdk watch and --hotswap, and why not use them in production?
cdk deploy --hotswap updates supported resources - Lambda code, Step Functions definitions, ECS task images and a few others - by calling service APIs directly instead of running a CloudFormation update. cdk watch watches your files and re-deploys (hotswapping where it can) on every save. Both turn a minutes-long loop into seconds.
The catch is in the docs' own wording: hotswap deliberately introduces drift. The stack template says one thing and the resource is running another. That is fine in a personal dev stack and wrong in any shared or production environment, where you want the CloudFormation record, rollback, and the change set to be the truth. --hotswap-fallback falls back to a full deployment when a change cannot be hotswapped. Follow-up: run a normal cdk deploy before you hand the stack to anyone else, to reconcile it.
21. How do you bring existing resources under CDK?
Three routes:
- Reference only:
fromXxxArn / fromLookup (question 13). The resource stays unmanaged. Right for shared resources another team owns.
cdk import: add the resource to your code with properties that match reality, then run cdk import; it uses CloudFormation resource import to adopt the existing resource into the stack. The resource must support import, and you typically set RETAIN while you do it.
cdk migrate: generate a whole new CDK app from an existing CloudFormation stack, a template, or resources scanned in the account. The output is mostly L1 constructs - a starting point you then refactor toward L2s carefully (question 18), not finished code.
Interviewers like a candidate who says "import, then prove it with a cdk diff that shows no changes, before refactoring anything".
22. How do you detect drift in a CDK app?
Drift is when the real resource no longer matches what the stack last deployed - someone edited a security group in the console, or a hotswap ran. cdk drift runs CloudFormation drift detection for your stacks and reports the differences; you can also run detection from the CloudFormation console or CLI.
What you do next is the actual answer. If the manual change was right, put it in code and deploy, so the code is the truth again. If it was wrong, deploy to overwrite it - but know that CloudFormation only pushes the properties it believes changed, so a deploy with no template change may not correct drift; changing the property in code or recreating the resource can be necessary. Long term: least-privilege console access, and a scheduled drift check in CI that alerts rather than fixes.
Part 4: Cross-stack references, aspects and escape hatches (23-29)
23. How do cross-stack references work in CDK?
You pass a construct from one stack into another as a prop:
const data = new DataStack(app, 'Data', { env });
new ApiStack(app, 'Api', { env, table: data.table });
When CDK sees the table used in a different stack in the same environment, it automatically creates a CloudFormation export in Data and an Fn::ImportValue in Api, and adds a stack dependency so Data deploys first. You never write the export yourself.
Across environments (different account or Region), exports do not work, so plain props fail. You either pass the value another way (SSM Parameter Store with a known name, or a static identifier), or opt into CDK's cross-region reference support where the construct supports it. Within one stack, references are plain Ref/GetAtt and none of this applies.
24. What is the cross-stack "deadly embrace", and how do you get out of it?
With a strong (export/import) reference, CloudFormation will not delete an export while any stack imports it. Now remove the table's use from ApiStack. CDK stops generating the export in DataStack and the import in ApiStack. Dependency order deploys DataStack first, which tries to delete the export - and fails, because ApiStack still imports it:
Export Data:ExportsOutputFnGetAttOrdersTable... cannot be deleted as it is in use by Api
The classic fix is two deploys. First, remove the usage from the consumer but keep the export alive by hand with this.exportValue(table.tableArn) in the producer, using the same export, and deploy both stacks. Second, remove the exportValue call and deploy again.
Recent CDK versions add reference strength. A weak reference makes the consumer read the producer's output with Fn::GetStackOutput instead of an import, so no export coupling exists. You can set it app-wide with the @aws-cdk/core:defaultCrossStackReferences context key, per resource with CrossStackReferences.of(resource).produce(...), or per usage with Stack.consumeReference(...). Existing strong references migrate in two phases: deploy with BOTH (the export stays, the consumer stops importing), then WEAK.
25. What are the alternatives to cross-stack exports?
- Put them in one stack. If two things always deploy together, splitting them only buys coupling problems.
- SSM Parameter Store. The producer writes
/app/prod/table-arn; the consumer reads it with ssm.StringParameter.valueForStringParameter() (resolved at deploy time) or valueFromLookup() (at synth time, cached). No CloudFormation coupling, works across stacks and apps, but nothing stops the producer deleting a value a consumer still needs - you own that contract.
- Deterministic names. Some teams use known names for shared resources. Simple, but you give up replacement safety (question 8).
- Weak references (question 24).
A good answer names the trade-off: exports give you referential integrity enforced by CloudFormation at the cost of rigidity; everything else gives you flexibility at the cost of an implicit contract.
26. What are aspects?
An aspect is a visitor applied to a construct subtree. Before synthesis, CDK calls visit(node) on every construct in the scope you attached it to.
class RequireBucketVersioning implements cdk.IAspect {
visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket && !node.versioningConfiguration) {
cdk.Annotations.of(node).addError('Buckets must enable versioning');
}
}
}
cdk.Aspects.of(app).add(new RequireBucketVersioning());
cdk.Tags.of(app).add('cost-center', 'payments');
Use them for cross-cutting rules: tagging, compliance checks, forcing encryption or a permissions boundary. Errors added through Annotations fail synthesis. The cdk-nag project packages rule sets (AWS Solutions, HIPAA, NIST and others) as aspects. Gotcha: aspects that mutate resources are powerful but can surprise readers, because the template no longer matches the code they see. Prefer aspects that validate, and mutate only for genuinely universal rules like tags.
27. What is an escape hatch?
When an L2 does not expose a CloudFormation property you need, you drop to the underlying L1 and set it:
const bucket = new s3.Bucket(this, 'Logs');
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;
cfnBucket.analyticsConfigurations = [{ id: 'all', storageClassAnalysis: {} }];
cfnBucket.addOverride('Metadata.Owner', 'payments-team');
cfnBucket.addPropertyDeletionOverride('BucketEncryption');
In order of preference: the L2 prop if it exists, the typed L1 property, then raw overrides. addOverride can touch any part of the resource, including DependsOn and Metadata. The follow-up: escape hatches bypass the L2's assumptions, so an L2 helper that reads the original configuration may now be wrong. Comment every override with why, and remove it when the L2 catches up.
28. When do you need a custom resource?
When CloudFormation cannot do something during a deployment: seeding a table, calling a third-party API, reading a value from another Region, emptying a bucket before deletion. A custom resource is a Lambda that CloudFormation invokes on Create, Update and Delete.
CDK gives two levels. AwsCustomResource makes a single AWS SDK call with no Lambda code for you to write. cr.Provider handles the response protocol and optional async polling for your own Lambda.
What interviewers probe: idempotency (CloudFormation can send Update and Delete you did not expect, including during a rollback), the physical resource ID (returning a new one on Update tells CloudFormation it was a replacement, so it sends a Delete for the old one), and failure handling - a handler that throws without responding leaves the stack waiting until it times out. Treat custom resources as a last resort; each one is deploy-time code you must maintain.
29. How do grant* methods work, and why do interviewers like them?
L2 constructs expose methods like table.grantReadWriteData(fn), bucket.grantRead(role) and queue.grantSendMessages(fn). Each adds the minimal IAM statement for that access pattern to the grantee's policy, scoped to that resource's ARN, and where the resource has a resource policy or KMS key it updates those too.
const table = new dynamodb.Table(this, 'Orders', { });
const fn = new lambda.Function(this, 'Handler', { });
table.grantReadWriteData(fn);
Interviewers like them because they test whether you understand least privilege without hand-writing JSON. Follow-ups: grants across stacks create cross-stack references (question 23); grants on imported resources only update the side you own (question 13); and cdk diff lists every IAM change under its security section, which is the place to review them.
Part 5: Testing and CI/CD (30-34)
30. How do you unit test CDK code?
With the aws-cdk-lib/assertions module. You synthesize a stack in the test and assert on the template.
import { App } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
test('orders table is encrypted and retained', () => {
const app = new App();
const stack = new DataStack(app, 'Test');
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::DynamoDB::Table', 1);
template.hasResourceProperties('AWS::DynamoDB::Table', {
SSESpecification: { SSEEnabled: true },
});
template.hasResource('AWS::DynamoDB::Table', {
DeletionPolicy: 'Retain',
});
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: { Statement: Match.arrayWith([
Match.objectLike({ Action: Match.arrayWith(['dynamodb:PutItem']) }),
]) },
});
});
These fine-grained assertions encode intent: "this table must be encrypted". Annotations.fromStack() lets you assert that an aspect raised an error. Python, Java, C# and Go have the same module. Fine-grained tests are the ones worth gating on.
31. Snapshot tests: useful or noise?
A snapshot test serializes the whole template and fails when anything changes:
expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
Useful as a tripwire for unintended changes during a refactor or a library upgrade: if a pure rename produces a template diff, you want to know. Noise when every legitimate change, including asset hashes and CDK version bumps, forces a snapshot update that reviewers approve without reading.
The balanced answer: fine-grained assertions for the properties you care about (encryption, retention, IAM scope, alarms), snapshots only where "nothing should change" is the claim, and asset hashes stabilized or excluded. Beyond unit tests, integ-tests (the @aws-cdk/integ-tests-alpha module) deploys a real stack, runs assertions against live resources, and tears it down - slow, so you keep a few for critical paths.
32. What are CDK Pipelines?
aws-cdk-lib/pipelines is a construct library for CI/CD that deploys CDK apps through CodePipeline. You describe the source and synth step and add Stages; it builds the pipeline for you.
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
crossAccountKeys: true,
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.connection('org/repo', 'main', { connectionArn }),
commands: ['npm ci', 'npm test', 'npx cdk synth'],
}),
});
pipeline.addStage(new MyAppStage(this, 'Staging', { env: stagingEnv }), {
post: [new pipelines.ShellStep('Smoke', { commands: ['./smoke-test.sh'] })],
});
pipeline.addStage(new MyAppStage(this, 'Prod', { env: prodEnv }), {
pre: [new pipelines.ManualApprovalStep('PromoteToProd')],
});
Key behaviors: self-mutation - a pipeline stage re-deploys the pipeline itself from the new commit, so adding a stage in code adds it to the pipeline on the next run; synth once, the same cloud assembly is promoted through every stage; and pre/post steps for approvals and tests. The pipeline only needs to be deployed manually once.
33. How do you deploy to multiple accounts from one pipeline?
Bootstrap every target account and Region, and in each one trust the pipeline (tooling) account:
cdk bootstrap aws://222222222222/us-east-1 \
--trust 111111111111 \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
--trust lets the pipeline account assume the target account's deploy and publishing roles. The execution policy decides what CloudFormation may create there; in production, replace AdministratorAccess with a scoped policy and consider a permissions boundary. In the pipeline, set crossAccountKeys: true so the artifact bucket uses a KMS key the other accounts can be granted access to.
Follow-ups: context lookups in the target account need --trust-for-lookup (or a committed cdk.context.json); keep the pipeline in a dedicated tooling account; and give every Stage an explicit env so nothing deploys to the wrong place.
34. GitHub Actions or CDK Pipelines?
Both work. CDK Pipelines gives you self-mutation, built-in asset publishing, cross-account wiring and a CodePipeline that lives in AWS next to what it deploys. GitHub Actions (or GitLab CI, or anything else) is where many teams already run builds, with better PR integration and a familiar UI.
With an external CI, the shape is the same: synth once into an artifact, run cdk diff and post it to the PR, deploy that exact cdk.out with cdk deploy --app cdk.out per environment, using OIDC federation into an IAM role instead of long-lived access keys. The choice is about where you want your delivery tooling, not what CDK can do. The weak answer is picking one without mentioning artifact promotion or credentials.
Part 6: CDK compared (35-36)
| CloudFormation | AWS CDK | Terraform / OpenTofu | AWS SAM |
|---|
| Authoring | YAML/JSON | TS, Python, Java, C#, Go | HCL | YAML with serverless shorthand |
| State | Managed by CloudFormation | Managed by CloudFormation | State file you store and lock | Managed by CloudFormation |
| Scope | AWS (plus registry extensions) | AWS (via CloudFormation) | Multi-cloud and SaaS providers | AWS serverless |
| Preview | Change sets | cdk diff | terraform plan | Change sets |
| Rollback | Automatic on failure | Automatic (CloudFormation) | None - partial applies stay | Automatic |
| Abstraction | None | L2/L3 constructs, grants | Modules | Serverless resource types |
| Best for | Simple, auditable templates | AWS-only teams who want real code and tests | Multi-cloud, or one tool for AWS + SaaS | Small Lambda/API apps, local testing |
The honest answer depends on the team. CDK wins when you are AWS-only and developers own their infrastructure: loops, types, grants and unit tests are a real productivity gain. Terraform wins when you manage more than AWS (DNS at a third party, Datadog, GitHub, a second cloud) or already have a large Terraform estate, and its plan is closer to real state because it refreshes against the provider. SAM is fine for a small serverless app and its local tooling is good, but most teams outgrow it. Raw CloudFormation is still what StackSets and some compliance teams want. Interviewers mark you down for "CDK is better" with no conditions.
36. When would you not use CDK?
- Multi-cloud or SaaS-heavy estates, where one Terraform workflow covers everything.
- Teams without programming habits. A CDK app can become a clever framework only its author understands; declarative HCL limits how clever people can be.
- Very large stacks. Everything still lands in CloudFormation, so its per-stack resource quota and deployment speed apply; a giant app needs careful stack design.
- Resources CloudFormation does not support yet, where you would end up writing custom resources for core infrastructure.
- An existing, working IaC setup where a rewrite buys little.
The senior framing: the decision is about who owns the infrastructure and how it is reviewed, not about which syntax is nicer.
Part 7: Scenarios - design or debug this (37-40)
37. A teammate's PR moves the orders table into a new DataLayer construct. cdk diff shows the table being destroyed and recreated. What do you do?
Stop the merge. The move changed the construct path, so the logical ID changed, and CloudFormation will create a new, empty table and delete (or orphan) the old one. Options, in order:
- Run
cdk refactor --unstable=refactor --dry-run. If the PR is a pure move, it should map old ID to new ID and apply it without replacement. Ship the refactor alone, before any functional change.
- If
cdk refactor is not available to the team, pin the old logical ID with overrideLogicalId, or restructure so the table is the construct's Default child.
- Confirm with
cdk diff: the table should show no change at all.
Then make it hard to repeat: RETAIN on every stateful resource, a CI check that fails on any replacement of a stateful resource type, and a test that asserts the table's logical ID.
38. You deleted an unused shared bucket from the data stack. Deploy fails with "Export ... cannot be deleted as it is in use by ...". Walk through the fix.
This is question 24 in production. The consumer still imports the bucket's export, and dependency order deploys the producer first.
- Revert the producer so the bucket exists again.
- Remove every use of the bucket from consumer stacks, and add
this.exportValue(bucket.bucketName) (and any other attributes that were exported) in the producer, so the same exports survive.
- Deploy all affected stacks. Consumers drop their imports; the exports stay.
- Remove the
exportValue calls and the bucket, and deploy again.
If the team is on a recent CDK, the forward-looking fix is to migrate shared references to weak (BOTH, deploy, then WEAK, deploy), so the next removal has no export to fight. Also check the bucket's removal policy before step 4 - a retained bucket outlives the stack and needs a decision about its data.
- One app, one
AppStage containing a stateful DataStack (tables, buckets, keys - RETAIN, termination protection on) and a stateless ApiStack (Lambdas, API Gateway, alarms). Splitting stateful from stateless means the stack you redeploy constantly cannot replace data.
- Three Stage instances - dev, staging, prod - each with an explicit
env in its own account. Config differences come from a typed per-environment config object, not if (env === 'prod') scattered through constructs.
- CDK Pipelines in the tooling account: synth with tests and cdk-nag, deploy staging, run smoke tests as a
post step, manual approval, deploy prod.
- Bootstrap each workload account with
--trust for the tooling account and a scoped execution policy.
- Testing: fine-grained assertions for encryption, retention and IAM; a handful of integration tests in dev.
- Shared values between
DataStack and ApiStack as direct references within the Stage, with weak references or SSM if you expect to restructure.
The interviewer is checking the stateful/stateless split, explicit environments, and promotion of one artifact.
40. A new CI job fails with an error about the bootstrap stack version, or about a missing /cdk-bootstrap/hnb659fds/version parameter. Diagnose it.
Stacks synthesized with the default synthesizer include a check that reads the SSM parameter /cdk-bootstrap/<qualifier>/version (hnb659fds is the default qualifier) and fails the deploy if the bootstrap stack is missing or older than required.
Work through it:
- Wrong account or Region. The job's credentials point somewhere that was never bootstrapped. Check with
aws sts get-caller-identity and the stack's env.
- Never bootstrapped, or bootstrapped long ago. Run
cdk bootstrap aws://ACCOUNT/REGION with the same options as before (trust, execution policies) to upgrade the stack. Features like cdk refactor need a recent bootstrap template too.
- Custom qualifier mismatch. If the environment was bootstrapped with
--qualifier, the app must set the same qualifier (in cdk.json context or the synthesizer props), or it looks for a parameter that does not exist.
- Permissions. The role the job uses must be able to read that parameter and assume the bootstrap roles; a cross-account job also needs
--trust on the target.
Record the bootstrap command and its options in the repo, so re-bootstrapping does not drop a trust relationship.
How to practice for an AWS CDK interview
- Build a two-stack app and break it on purpose. Rename a construct holding a table and read the diff. Remove a cross-stack reference and hit the export error. Ten minutes of each beats any amount of reading.
- Run
cdk synth and read the template. Find the logical IDs, the asset references, the IAM policies your grant* calls produced. Interviewers ask "what does this generate?" and the template is the answer.
- Write fine-grained assertion tests for a stack you already have - encryption, retention, IAM scope. That habit is what a senior CDK reviewer looks for.
- Deploy a small CDK Pipeline across two accounts if you can, so bootstrapping,
--trust and self-mutation are things you have seen rather than recited.
- Prepare your CDK vs Terraform answer with conditions. Name the situations where you would choose each, and one thing you would miss from the other.
- Say the CloudFormation consequence out loud. For every CDK answer, add what CloudFormation will do: update, replace, roll back, or refuse.
Keep going
- Drill the services: the AWS question bank and the DevOps question bank cover the services and delivery practices CDK sits on, as timed multiple-choice, and the free daily challenge gives you one question a day with no account.
- Compare with Terraform: the Terraform question bank, the Terraform and IaC cheat sheet and Top 50 Terraform interview questions cover state, plans and modules for the comparison questions.
- Keep the CLI close: the AWS CLI cheat sheet for the
sts, cloudformation and ssm commands you use when debugging a deploy.
- Go wider: the AWS Solutions Architect track and the SRE and DevOps prep track, plus Top 50 AWS interview questions and Top 50 DevOps interview questions.
- Certification angle: CDK, CloudFormation and pipelines feature in the AWS DevOps Engineer Professional practice exam; the DOP study path explains how to approach it.
- Practise the follow-ups: the chat-based AI mock interview pushes on the "and what does CloudFormation do then?" questions that decide a CDK round.
- Related guides: Top 50 AWS Lambda and Serverless Interview Questions 2026, Top 50 AWS Networking and VPC Interview Questions 2026, Top 50 DynamoDB Interview Questions 2026.
#awscdk #aws #infrastructureascode #cloudformation #devops #interviews