At this point realize that cloud formation is machine code, and cdk or terraform are the language you, a human, should use, like c/c++ but more like typescript.
What the machine needs is usually different from what you need. The machine solution sometimes comes first
A lot of people think CDK is a wrong turn. CF is just data. CDK generates CF without letting you do CF level transforms. It's an imperative wrapper to a inherently functional / declarative thing, and married to JavaScript to top it off. Generally wrapping a declarative, data based expression in a Turing complete API is a backwards direction - makes it less amenable to automated reasoning and risks intertwining incidental complexity of the GP programming environment like dependency churn and breakage in there.
Terraform is better but lots of shops are wary of relying on third party foundations.
I don't really get these arguemnts on why CDK is bad. I've got around 3k accounts, 300k aws resources, 1k devs, all working fine on CDK... I've never seen where functional / declarative change mattered. My issue with TFN is a half baked turing language that I fight a lot, and CFN is terrible for IFs or massifly repaeated.
I talked with the team building CDK... their market analysis was essentially: SysDEs love CFN or declarative. Developers hate repeating themselves and lack of abstractions. I generally agree with the latter. The lack of real modules, dependency management, compilers, etc are a put off for CFN and TFN for me. I'm loving CDK.
You could have both abstractions and DRY without mixing it in imperative library calls and opaque state. You could then write eg tools that apply changes robustly across all your stacks, inspect for security risks, check against custom schemas and policies, reusable things that compose better, etc.
I'm doing all of that actually... I don't see how "functional" and "declarative" is in any way helping... You're making statements without getting into the details.
For instance we run a linter for security issues and best practices during the cdk build... We have tools applying changes across our abstractions and against our non-abstractions.
In a declarative form the changes don't make it back into the declarative form unless you're pullling cfn state back into say git, or harder reversing it back into TFN state.
Think for example how you would robustly check that resources of certain types are labeled in a way conforming to a given schema, or that would rewrite the label values in your stacks to a newer format. Linting CDK code can't get you this.
It's possible of course that I don't know enough about CDK and these examples have CDK solutions.
Imperative and declarative are not hard requirements for this but they make the domain easier to reason about and they are a good fit to the CF model of configuration-as-data instead (vs CDK's configuration-as-imperative-code).
Cdk linters can 100% do this. They can operate at the various construct levels including level 1 eg generated cloud formation. At this point it's just making a linter/visitor that says "if type is taggable then it must have this tag" and bob's your uncle. The l1 stuff is generated from the cfn schema and so will have tags correctly.