Fix TechDebt: ClickOps to CDK made easy with Kiro
Fix your technical debt by importing console-created resources into AWS CDK managed stacks. What was a daunting task in the past today is easy - thanks to Kiro.
Every AWS account has them. The Lambda function someone created during an incident. The DynamoDB table that started as a proof of concept and quietly became load-bearing. The IAM role with a name like
my-function-role-a1b2c3 that nobody remembers creating.They work. That's the problem — they work well enough that nobody touches them, and the knowledge of how they were configured leaves with the person who clicked them into existence.
Three conversations you may recognize:
The colleague who built this has left. Nobody knows why the timeout is 47 seconds — and nobody dares touch it.
We need the identical setup in eu-central-1 by Friday. Two days of clicking later, it still isn't identical.
It broke after someone "just changed one setting." No idea which one.
The fix is Infrastructure as Code. The obstacle is that most IaC guidance assumes you're starting from nothing, and you aren't — you're starting from something that already exists and is already serving traffic.
This article walks the path from a console-created resource to one managed by AWS CDK, without deleting and recreating it.
A quick word on what ClickOps actually costs you
"ClickOps" is building resources by clicking through the AWS Console. It's the fastest way to get started and there's nothing shameful about it.
What it costs you is not auditability — CloudTrail records who called what, so console actions are traceable after the fact. What you lose is different, and more subtle:
- No review before the change lands. There's no pull request, no second pair of eyes, no automated check. The change is simply live.
- No versioned record of intended state. CloudTrail tells you a setting changed. It doesn't give you something you can diff, roll back, or reason about as a whole.
- No repeatability. Another Region, another account, a rebuild after an incident — all of it is manual again.
Those three gaps are what infrastructure as code (IaC) with AWS CDK closes.
The problem: your resource already exists
Here's where the standard tutorials stop being useful.
CDK is built to create resources. If you write a construct describing your existing function and run
cdk deploy, CloudFormation tries to create it and fails: "already exists."So you have two options.
Option A: delete it and recreate it
Tempting, because it's less work. Genuinely fine for a scratch resource. On anything real, it bites in three ways:
- The ARN changes. An ARN is the globally unique identifier other things reference. A new one silently breaks IAM policies, event source mappings, and anything with the old value baked into configuration.
- Data is gone. Tables, buckets, and volumes hold state you can't rebuild from a template.
- There's downtime while the resource is torn down and replaced.
Option B: import it
cdk import tells CloudFormation to start managing a resource that already exists. The resource never moves. Same ARN, same data, no downtime. CloudFormation adopts it under a Retain deletion policy, so nothing is created or destroyed in the process.The rest of this article is Option B.
Step 1: Describe the resource in code
This is the part that surprises people: you write the CDK code yourself, by hand, describing the resource as it already is. Nothing is exported from AWS for you.
- Start from a project. A new one (
cdk init app --language typescript) or the CDK project you already have. If you already have one, just add the construct to an existing stack — there's no requirement for a greenfield project. A separate stack for imports keeps the blast radius small, but it's a preference, not a rule. - Discover the live settings. Names, size, configuration, permissions.
aws lambda get-function,aws iam get-role, and friends. - Write code that matches exactly. Same physical names. Same memory, timeout, runtime, role path.
- Run
cdk synth.
That last step deserves a note, because it confuses everyone the first time:
cdk synth reads your code, not AWS. It exists here so you can see the logical IDs CDK generated for the constructs you wrote. You need those next.Two rules matter at this stage:
- Match physical names exactly. Without an explicit
functionName,roleName, orlogGroupName, CDK generates its own unique names and the import won't match anything. - Avoid
grant*()helpers for now. Convenient methods liketable.grantWriteData(fn)generate an extra IAM policy resource that doesn't exist in AWS yet, and the import fails with an unresolved-dependency error. Spell permissions out as inline policies to get through the import, then refactor afterwards.
Step 2: Build the resource mapping
The mapping file connects two names for the same thing:
- Logical ID — CDK's internal handle, invented during synthesis (
MyFnABC123) - Physical name — what the resource is actually called in AWS (
my-fn)
Find the logical IDs in the synthesized template. The keys under
Resources: are what you want:Then pair them by hand. CDK does not generate this file for you:
Each resource type has its own identifier key — Function uses
FunctionName, Role uses RoleName, LogGroup uses LogGroupName, S3 uses BucketName, SQS uses the full QueueUrl. Get one wrong and the import either fails outright or tries to replace the resource.Step 3: Import, then read the diff carefully
That diff is the whole verification story, so read it properly:
- Only
[-] DeletionPolicymeans the import is faithful. That line is CloudFormation's import safety net being handed over to normal stack management. A stack description and aBootstrapVersionparameter appearing are also normal. - A property change, or worse a replacement, means your code doesn't match reality — usually a physical name. Fix the mapping or the construct before you deploy.
Then
cdk deploy once to normalise ownership, and invoke the resource to confirm it still behaves.Step 4: You only do this once
Worth being explicit, because people assume otherwise: import is scaffolding, not a new way of working. It runs once per resource and then gets out of the way. Every future change goes through ordinary
cdk deploy. Once the diff is clean, resource-mapping.json has served its purpose and can be deleted — deployments never read it. Some teams keep it committed as a record of what was adopted and when, which is a reasonable habit.What changes from this point:
- The code is the source of truth. If someone edits the resource in the console afterwards, the next
cdk diffshows it as drift, and a deploy pulls it back to whatever the code says. That discipline is the thing you were buying. - Now you can improve it safely. Set log retention instead of infinite. Add tags and alarms. Upgrade an end-of-life runtime. Swap those inline policies for
grant*()helpers. Delete the console's orphaned managed policy. Each change is a reviewedcdk diffaway.
Do the improving after the import is verified stable, not during. Import should be a no-op adoption; modernization is a separate, deliberate step.
When this doesn't apply
Not every resource type is importable. CloudFormation supports import for many types but not all. Lambda event source mappings, resource-based permissions, API Gateway definitions, CloudWatch alarms, and EventBridge rules are generally easier to define fresh in CDK and let it replace the manually-created equivalents.
Making it repeatable
Everything above is a defined sequence with defined checks, which makes it a good candidate for automation. I wrote a Kiro skill for this workflow — written-down expertise (a
SKILL.md with steps, commands, and guardrails) that an AI agent picks up on demand. Point the agent at a resource and it discovers the configuration, asks the judgment calls, writes the stack, builds the mapping, runs the import, and checks the diff.Try it on one resource
Pick a single non-production resource this week. A Lambda function is a good first target: small, self-contained, and it drags its execution role and log group along so you get practice importing a set rather than one thing.
Walk it through by hand the first time. You'll understand the diff output far better for having done it, and you'll know what "faithful import" actually looks like before you automate it.
Once you've done one by hand, try the
clickops-to-iac skill I built for this demo — it targets Lambda functions and their attached role and log group. Point it at one of your own functions and watch it discover the config, write the stack, and run the import. (Reminder: it's a demo skill). Feel free to modify it. Just ask Kiro to assist you with that task.The console isn't the enemy. Leaving things there permanently is.
Here my sample skill file:
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article