You already know roughly what AWS CDK is; how it relates to CloudFormation and Terraform is covered separately. What you want here is a working deployment in your own account, and a clear picture of why cdk deploy sometimes fails with a credentials error you didn't expect.
This AWS CDK getting started guide gets you there in both TypeScript and Python, and names what most tutorials skip: the chain of tools and credentials between the command you type and the account it touches. That chain is where first deploys go sideways, not the CDK code.
Budget about an hour. Here's exactly where that hour goes.
What You'll Build and How Long It Takes
By the end, you'll have a real Lambda function with a public URL running in your AWS account, deployed and destroyed at least once, in the language of your choice. Here's the honest breakdown of aws cdk getting started, step by step:
| Step | What you do | Est. time |
|---|---|---|
| Check prerequisites | Confirm Node.js, the AWS CLI, and working credentials | 5 min |
| Understand the auth chain | Know what connects cdk deploy to your account before you run it | 5 min |
| Scaffold your app | cdk init app --language typescript (or python) | 5 min |
| Bootstrap your account | Run cdk bootstrap once, in its simplest form | 5 min |
| Build, diff, and deploy | Add a Lambda function + function URL, then ship it | 25 min |
| Clean up | cdk destroy and check for anything left behind | 5 min |
That's about 50 minutes of hands-on work, call it an hour with reading and the inevitable typo. To be straight about the starting line: that clock assumes a workstation that already has Node, the AWS CLI, and the CDK CLI installed with credentials that work. Setting those up from scratch is its own job and isn't in the budget above. If cdk --version already runs cleanly, skip ahead to Scaffold your first app, or jump straight to Build, Diff, and Deploy for the deploy loop.
The CLI has grown recently too: cdk refactor reorganizes stacks without CloudFormation replacing resources, and cdk drift detects drift against what's deployed. Neither belongs in a first deploy, but older tutorials predate both.
Before You Start: Prerequisites
Three things before any command below works: a supported Node.js version, the AWS CLI configured, and an AWS account you can deploy into.
AWS's onboarding guidance is Node.js 22.x or later, since every CDK language binding runs on the same Node.js backend regardless of whether you write TypeScript, Python, or Go. If you're mid-project on Node.js 20.x you're not stuck: it stays a fully supported CDK runtime through October 30, 2026 and satisfies the version floor for both the CLI and the library, so no engine warnings or errors today. Install 22.x fresh, keep 20.x if you'd rather not touch a working toolchain.
You'll also need the AWS CLI installed and configured with real credentials, and the CDK CLI itself installed globally via npm. Neither of those is this guide's job. For install mechanics, version pinning, and the "cdk: command not found" fix, see our full CDK install guide. If cdk --version doesn't print a version number after installing, that's the first place to look.
One version detail that explains a confusing error you might hit later: the CDK CLI (aws-cdk on npm, currently 2.1133.0 as of this writing) and the construct library (aws-cdk-lib, currently 2.262.2) have followed independent release cadences since February 2025, so those mismatched numbers are intentional rather than a sign something is wrong. Upgrading the CLI ahead of the library is always safe. Downgrading it below what your cloud assembly requires is what breaks:
Cloud assembly schema version mismatch: Maximum schema version supported is 3.0.0, but found 4.0.0.
Please upgrade your CLI in order to interact with this app.
If you see that, upgrade the CLI, not the library.
cdk deploy has no credential store of its own. It reads the same ~/.aws files that aws configure writes.
The Real Chain Behind cdk deploy
Three separate mechanisms decide where your stack lands, and conflating them is why first deploys go to the wrong account.
1. Where your credentials come from
cdk deploy doesn't have its own credential store. It reads the same shared credentials and config files under ~/.aws that the AWS CLI uses. So the chain of trust looks like this: CDK CLI → your ~/.aws credentials and config → an IAM Identity Center (SSO) profile or IAM user → your target AWS account and Region. If any link isn't set up, the failure surfaces as a CDK error even though CDK itself did nothing wrong.
Two precisions. CDK doesn't shell out to the AWS CLI and doesn't strictly require it; AWS recommends the CLI for writing those files (aws configure, aws configure sso) and you'll want it for aws sso login, but hand-edited files or environment variables work too. And same files doesn't mean identical behavior: CDK resolves credentials through the AWS SDK for JavaScript, which AWS's CLI reference warns "might behave somewhat differently", so treat aws sts get-caller-identity as a strong hint rather than a guarantee.
For individuals and small teams, AWS recommends IAM Identity Center: aws configure sso once to register a profile, then aws sso login before deploying. A plain IAM user with aws configure works the same way from CDK's perspective, minus the SSO session step.
2. Which account and Region you deploy to
This is a separate question from which credentials you deploy with, and it resolves through its own documented precedence. Highest wins:
- A hardcoded
envblock on theStackconstruct. CDK_DEFAULT_ACCOUNTandCDK_DEFAULT_REGION, referenced inside thatenvblock.- The profile you pass with
--profile. - The
defaultprofile in your credentials and config files.
Note the direction of item 3: --profile beats environment variables, not the reverse. That catches people who export AWS_PROFILE, pass --profile in a script, and expect the export to win.
Those two are injected by the CDK CLI itself, populated from whichever profile it just resolved, which is what makes env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION } a common scaffold: the stack follows your active profile instead of pinning to one account.
Use the CDK_-prefixed names specifically. AWS_DEFAULT_REGION is an SDK-level region override, and there is no AWS_DEFAULT_ACCOUNT at all, so an env block reading that name silently receives undefined. The stack then quietly reverts to environment-agnostic and takes Vpc.fromLookup down with it.
Leaving env off deliberately has the same effect: an environment-agnostic template that deploys wherever --profile points, and no logic branching on stack.region. Fine for a first deploy. For production AWS recommends the opposite, pinning the environment explicitly so a stack can't drift into the wrong account.
3. Which permissions actually execute
Once your credentials resolve, CDK doesn't deploy with your IAM permissions directly. It assumes IAM roles that were created during bootstrap, specifically a DeploymentActionRole to kick off the deployment and a CloudFormationExecutionRole to actually create and modify resources. By default, that execution role carries AdministratorAccess with no permission boundary attached. For a personal sandbox account, that's a reasonable default that keeps you from fighting permission errors on every new resource type. For anything shared, production, or compliance-relevant, it's worth knowing that default exists before you bootstrap a second or third account, since AWS's CDK security best practices guide explicitly calls out that trade-off and describes how to scope execution policies down. Our CDK best practices guide covers that scoping, plus the other production hardening steps, in more depth.
None of this changes what you do for a first deploy. It just means you'll recognize the failure mode when credentials expire mid-session, or when a colleague asks why their cdk deploy can create anything in the account.
Scaffold Your First App with cdk init
With the auth chain out of the way, it's time to create something for it to deploy. Pick one language below and stick with it for the rest of this guide; the commands and concepts map directly between the two.
cdk init ships three templates: app (an empty CDK application, and the default if you omit the flag), sample-app (adds a starter SQS queue and SNS topic), and lib (for building a reusable construct library, and every documented AWS example pairs it with --language=typescript, though nothing in the docs states that pairing is enforced). You'll use app.
TypeScript
mkdir hello-cdk && cd hello-cdk
cdk init app --language typescript
This scaffolds a small, predictable project:
hello-cdk/
├── bin/hello-cdk.ts # App entry point - instantiates App and your Stack
├── lib/hello-cdk-stack.ts # Your stack class - constructs go here
├── cdk.json # Tells the CLI how to run your app
├── package.json # Dependency manifest
└── tsconfig.json
cdk.json points the CLI at how to execute your app:
{ "app": "npx ts-node bin/hello-cdk.ts" }
One structural detail worth noticing in package.json: aws-cdk-lib and constructs live under dependencies, because your app's code imports them directly. aws-cdk (the CLI) lives under devDependencies, because it's a tool you run, not a library your code calls.
Python
mkdir hello-cdk && cd hello-cdk
cdk init app --language python
source .venv/bin/activate
pip install -r requirements.txt
Python gets the same file structure with Python conventions: app.py as the entry point, a hello_cdk/hello_cdk_stack.py stack module, and the extra virtual-environment activation step above that TypeScript doesn't need. cdk init creates the .venv for you; you just have to activate it and install dependencies before your first cdk synth.
Whichever language you picked, cdk init also runs git init automatically if Git is available locally, so you get a clean starting commit for free.
Point CDK at Your Account and Bootstrap Once
Every AWS account and Region combination needs to be bootstrapped exactly once before CDK can deploy anything into it. Bootstrap creates the S3 bucket, ECR repository, and IAM roles CDK relies on for every deployment that follows, including the CloudFormationExecutionRole mentioned above.
In its simplest single-account form:
cdk bootstrap
Run without an explicit environment argument, the CLI figures out the target account and Region from your currently configured credentials. If you skip this step and deploy anyway, you'll hit an error that looks like this:
❌ Deployment failed: Error: BootstrapExampleStack: SSM parameter /cdk-bootstrap/hnb659fds/version not found. Has the environment been bootstrapped? Please run 'cdk bootstrap'
That message is your cue to run cdk bootstrap, not a sign anything is broken. Bootstrapping is idempotent and safe to re-run whenever a newer version ships; it upgrades the stack in place or no-ops if you're already current. One thing worth knowing before you bootstrap several accounts "just to try things": the S3 bucket and ECR repository it creates incur ongoing charges per environment, not a one-time cost.
This guide deliberately stops at the simple case. For what bootstrap actually creates resource by resource, multi-account setups, the --trust flag, and fixes for the half-dozen named bootstrap errors, see our dedicated CDK bootstrap guide.
Build, Diff, and Deploy Your First Resource
Time to ship something real: a Lambda function with a public function URL, the same minimal pattern AWS's own first-app tutorial uses. Open your stack file and add this.
TypeScript (lib/hello-cdk-stack.ts):
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class HelloCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const fn = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromInline(
'exports.handler = async () => ({ statusCode: 200, body: "Hello from CDK" });'
),
});
const fnUrl = fn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE });
new cdk.CfnOutput(this, 'Url', { value: fnUrl.url });
}
}
Python (hello_cdk/hello_cdk_stack.py):
from aws_cdk import Stack, CfnOutput
from aws_cdk import aws_lambda as _lambda
from constructs import Construct
class HelloCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
fn = _lambda.Function(self, "HelloHandler",
runtime=_lambda.Runtime.PYTHON_3_14,
handler="index.handler",
code=_lambda.Code.from_inline(
"def handler(event, context):\n return {'statusCode': 200, 'body': 'Hello from CDK'}"
),
)
fn_url = fn.add_function_url(auth_type=_lambda.FunctionUrlAuthType.NONE)
CfnOutput(self, "Url", value=fn_url.url)
Now run the loop:
| Command | What it does | When you'd run it |
|---|---|---|
cdk synth | Generates the CloudFormation template without touching AWS | Before every deploy, or just to inspect what your code produces |
cdk diff | Compares your synthesized template against what's actually deployed | Before deploying, to catch surprises early |
cdk deploy | Deploys the stack via CloudFormation | Once you're happy with the diff |
cdk destroy | Deletes the stack and everything CloudFormation tracks for it | When you're done (see cleanup below) |
Run cdk deploy. Expect one approval prompt on this first deploy: CDK's default (--require-approval broadening) only stops you when a change adds IAM permissions or security-group rules, and this stack adds both, a new Lambda execution role and a public lambda:InvokeFunctionUrl grant. Review the diff and type y. Once it finishes, the function URL prints in your terminal as a stack output. Open it in a browser or curl it, you should see your handler's response.
Note that both snippets above rely on your default account and Region resolving through the auth chain from earlier. If you hardcode an env block on the stack the way AWS's own tutorials sometimes show for clarity, remember that's explicitly not the recommended pattern once you're past a single personal sandbox.
If cdk deploy fails immediately with an SSM parameter error, you skipped bootstrap, jump back to the previous section. If it fails with an access-denied error, your credentials likely expired, re-run aws sso login or check your profile.
Go One Step Further (Optional)
Everything above deploys a single resource. CDK's actual value shows up once you compose several resources together, and this section is entirely skippable if you're protecting your time budget, it doesn't block anything that follows.
A natural next step from a standalone Lambda function is wiring an S3 bucket to trigger it on upload. The construct-level API for this is bucket.addEventNotification() paired with s3n.LambdaDestination:
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
const bucket = new s3.Bucket(this, 'UploadBucket');
bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(fn));
from aws_cdk import aws_s3 as s3
from aws_cdk import aws_s3_notifications as s3n
bucket = s3.Bucket(self, "UploadBucket")
bucket.add_event_notification(s3.EventType.OBJECT_CREATED, s3n.LambdaDestination(fn))
Under the hood, CDK synthesizes a small custom resource that calls S3's notification API directly, sidestepping a circular-dependency problem that exists if you wire this up in raw CloudFormation. It also grants the invoke permission for you: run cdk synth on the stack above and you'll find an AWS::Lambda::Permission resource granting lambda:InvokeFunction to the s3.amazonaws.com principal that nothing in your code asked for. Good habit generally: when you want to know what a construct did, read the synthesized template rather than guessing. What CDK can't infer is whether your handler reads the object itself. If it fetches the uploaded contents, add bucket.grantRead(fn). The hello-world handler above never calls GetObject, so it doesn't need that grant and is better without it. If you also want to lock down the bucket itself (encryption, versioning, blocking public access), our guide to creating an S3 bucket with CDK covers that configuration in detail.
There's an alternative entry point to the same wiring: fn.addEventSource(new S3EventSource(bucket, {...})), from the Lambda side instead of the bucket side. Neither is documented as the standard. Use addEventSource when you're already wiring other event sources in one stack, addEventNotification when a bucket fans out to several destination types.
If you'd rather not hand-write even that, AWS Solutions Constructs ships a pre-built pattern: @aws-solutions-constructs/aws-s3-lambda (Python: aws_solutions_constructs.aws_s3_lambda), requiring aws-cdk-lib ^2.260.0 or later. It wires the same event source but adds S3 access logging, KMS encryption, and a least-privilege Lambda role by default.
What makes composition like this manageable in the first place is that you're working with L2 constructs (s3.Bucket, lambda.Function), which wrap the low-level Cfn* resources with sane defaults instead of requiring you to write IAM policy JSON by hand. For the full breakdown of L1 versus L2 versus L3 constructs and when to reach for each, see our CDK construct guide.
Excalidraw diagram loading.
Clean Up and Avoid Surprise Charges
Run cdk destroy when you're done. It deletes the CloudFormation stack and everything it tracks, which covers the Lambda function and function URL from this guide. AWS's own first-app tutorial closes the same way, to stop the function URL running indefinitely.
There's one trap worth knowing before you add your own stateful resources later: by default, things like S3 buckets and DynamoDB tables are created with RemovalPolicy.RETAIN. cdk destroy doesn't delete them, it orphans them, detaching them from CloudFormation while leaving them running and billing. If you gave that resource a fixed physical name (bucketName, tableName), the orphan also blocks your next deploy with a naming conflict. With CDK-generated names you instead get a second resource quietly sitting beside the first.
If you added the optional S3 bucket from the previous section, switching to RemovalPolicy.DESTROY is necessary but not sufficient. CloudFormation cannot delete a bucket that still has objects in it, and this tutorial has you upload one to trigger the event. So a bucket configured with DESTROY alone leaves the stack in DELETE_FAILED and the bucket standing. Pair it with autoDeleteObjects, which provisions a small custom resource that empties the bucket first:
const bucket = new s3.Bucket(this, 'UploadBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
from aws_cdk import RemovalPolicy
bucket = s3.Bucket(
self,
"UploadBucket",
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True,
)
Both settings belong in throwaway stacks only. AWS marks this combination as not recommended for production in its own samples, for the obvious reason: it turns a stack deletion into unrecoverable data loss. If you'd rather not grant that, empty the bucket by hand first.
If you're scripting cleanup and don't want the confirmation prompt, add --force (or -f).
The bootstrap stack stays behind, on purpose
Open the CloudFormation console after cdk destroy and you'll still see a stack called CDKToolkit. That isn't a failed cleanup. cdk destroy removes only the stack you deployed; CDKToolkit is the separate stack cdk bootstrap created, holding a staging S3 bucket, an ECR repository, and the deployment roles from earlier. It's deliberately shared, so every CDK app in that account and Region reuses it rather than bootstrapping again.
That persistence is what the modest ongoing bootstrap cost refers to. For a sandbox you keep using, leave it.
Getting an account genuinely back to zero takes three steps, because deleting the stack is not enough: the bootstrap bucket and repository are created with a RETAIN policy, so CloudFormation detaches them instead of deleting them. Empty the staging bucket and ECR repository, delete the CDKToolkit stack, then delete the now-orphaned bucket and repository yourself. Skip that last step and they stay in the account, still billing, and their fixed cdk-hnb659fds-* names will collide if you ever bootstrap that account again.
Only in an account you're done with, though: deleting it breaks every other CDK app in that account and Region until each is re-bootstrapped. Our CDK bootstrap guide covers what lives in that stack.
Skip the Boilerplate on Your Next Project
What you've built is a deliberately bare cdk init app: one stack, no pipeline, credentials from whatever profile you're using. Right shape for learning, wrong shape for anything a team depends on. The gap is plumbing you'd rather not write twice: CI/CD workflows, environment separation, and OIDC so your pipeline never holds long-lived AWS keys.
Our starter kits ship that plumbing already wired, using projen to generate and maintain the GitHub Actions pipelines so the configuration stays in code instead of drifting inside YAML nobody owns.
The same setup with a Python-native project layout:
Both authenticate to AWS through GitHub's OIDC provider rather than stored access keys, which closes off the most common way CDK credentials leak out of a team. For the TypeScript kit, setup and configuration are documented in the AWS CDK Starter Kit docs.
Frequently Asked Questions
Do I need to know CloudFormation before learning AWS CDK?
Should I use TypeScript or Python for my first CDK project?
Does cdk bootstrap cost money?
My project is still on Node.js 20, will it break?
Is AWS CDK itself free to use?
Key Takeaways
That's AWS CDK getting started, done properly: you now have a deployed Lambda function sitting in your own AWS account, in whichever language you picked, and you tore it down cleanly. More importantly, you understand the chain that makes cdk deploy work: your AWS CLI credentials, resolved through an SSO profile or IAM user, authenticating into a specific account where bootstrap-created roles do the actual deploying. That's the part that keeps tripping people up long after they've memorized the commands.
From here, the natural next question is how to structure a CDK app as it grows past one stack and one file. Our guide to organizing CDK projects covers that directly, from starter layouts through enterprise-scale multi-stack apps.
Next step
Just Getting Started with CDK? Get It Reviewed Before It Grows
A second pair of expert eyes now saves a painful refactor later. Get actionable, PR-based feedback on your CDK code within 5 days.