ACK Resource Adoption from IaC
Objective
Convert existing IaC-managed AWS resources (CloudFormation, Terraform, Pulumi, Crossplane) into ACK adoption manifests so the resources come under Kubernetes/GitOps control without being recreated, modified, or deleted. Where the IaC uses reusable modules or nested/stacked structures, generate kro ResourceGraphDefinitions that preserve that composition as a first-class Kubernetes API.
Scope
Analyzes:
- CloudFormation templates (
.yaml, .yml, .json with AWSTemplateFormatVersion or Resources: top-level key), including nested stacks (AWS::CloudFormation::Stack)
- Terraform files (
.tf, .tf.json), including local and registry modules (module blocks)
- Pulumi programs (TypeScript/Python with
@pulumi/aws or pulumi_aws imports)
- Crossplane manifests: Managed Resources (
*.upbound.io, *.aws.crossplane.io), Composition + CompositeResourceDefinition (apiextensions.crossplane.io), and claim instances (references/crossplane-to-ack-kro.md)
Generates:
- One ACK adoption manifest per supported AWS resource, with
adoption-policy: adopt-or-create, deletion-policy: retain, and adoption-fields when the identifier lives in status
- kro
ResourceGraphDefinitions (kro.run/v1alpha1) for IaC modules/nested stacks that instantiate more than one resource, exposing module inputs as schema fields
ADOPTION_REPORT.md documenting every mapped resource, every skipped resource (with reason), discovery commands, and the recommended apply order
Supported ACK services (initial mapping table in references/iac-to-ack-mapping.md): EKS, EC2 (VPC, Subnet, SecurityGroup, RouteTable, InternetGateway), S3, DynamoDB, SQS, SNS, RDS, ElastiCache, IAM (Role, Policy), KMS, Lambda, ECR, MSK, OpenSearch, Secrets Manager, SSM, EventBridge, CloudFront, Route53.
Non-Goals (out of scope for this transformation):
- Executing
kubectl apply or touching any live cluster or AWS account - this transformation generates manifests and a report; the customer applies them following the documented order.
- Removing or altering the source IaC - the CloudFormation/Terraform/Pulumi code is read-only input. Decommissioning the old IaC (e.g.,
terraform state rm, CloudFormation stack deletion with retain policies) is documented as a manual post-adoption step in the report, never automated.
- Resources without a GA ACK controller - flagged in the report with the closest alternative (stay in IaC, or ACK controller roadmap link), never guessed.
- Installing ACK controllers or kro - prerequisites are documented in the report; installation is the customer's platform decision (EKS Capabilities vs Helm).
Constraints
Safety (non-negotiable)
- EVERY generated manifest MUST carry
services.k8s.aws/deletion-policy: "retain". A missing retain policy can cause real AWS resource deletion when a CR is removed - this is the single most dangerous failure mode of this transformation.
- EVERY generated manifest MUST use
services.k8s.aws/adoption-policy: "adopt-or-create" so applying it never fails on an existing resource and never duplicates it.
- Never generate a manifest with placeholder/guessed identifiers. If the resource identifier cannot be resolved from the IaC (e.g., a Terraform computed value or a CloudFormation
!Ref chain that requires runtime state), emit the manifest with an explicit # TODO(discovery): comment containing the exact AWS CLI discovery command, and list it in the report's "Requires Discovery" section.
- When an
adoption-fields identifier is unresolved, the services.k8s.aws/adoption-fields annotation itself MUST stay commented out inside the TODO(discovery) block. Never emit an active adoption-fields annotation carrying a placeholder value (TODO, REPLACE_ME, or similar) - an active placeholder instructs ACK to adopt a resource by a fake identifier, which either fails adoption or matches the wrong AWS resource. The manifest stays inert for that resource until a human supplies the real value.
- The source IaC files are never modified.
Fidelity
- The generated
spec must reflect the resource configuration declared in the IaC (adopt-or-create reconciles spec against AWS after adoption - a spec that diverges from reality causes unintended updates). When an IaC attribute has no ACK spec equivalent, document it in the report rather than dropping it silently.
- Preserve resource naming: the Kubernetes object name derives from the IaC logical name (kebab-cased), and the AWS-facing name field keeps the exact deployed value.
- Tags declared in IaC are carried into the ACK spec
tags field when the controller supports it.
kro Generation Rules
- Generate a
ResourceGraphDefinition ONLY when the IaC has a module/nested-stack instantiated as a unit (a Terraform module block, a CloudFormation nested stack, or a Pulumi ComponentResource). One-off flat resources get plain ACK manifests - do not wrap single resources in kro.
- Module input variables become kro
spec.schema.spec fields with types and defaults mapped from the IaC variable definitions.
- Cross-resource references inside the module (e.g., subnet referencing VPC ID) become kro CEL expressions (
${resource.status.field}), replacing the IaC-native references (!Ref, !GetAtt, Terraform interpolation).
- Each kro resource template inside the RGD still carries the adoption + retain annotations - adoption safety applies inside graphs exactly as it does for flat manifests.
- For module instances (multiple instantiations of the same module), generate ONE RGD plus one instance CR per instantiation, with the per-instance values.
Reporting
ADOPTION_REPORT.md must contain: inventory table (IaC resource -> ACK Kind -> manifest file -> adoption-fields needed), skipped resources with reasons, discovery commands for unresolved identifiers, prerequisite checklist (controllers, feature gate, IRSA/Pod Identity permissions per service), recommended apply order (dependency-sorted: IAM/KMS -> network -> data stores -> compute), and post-adoption IaC decommission guidance.
- Every generated file must be listed with its source IaC file and line reference.
Workflow
Phase 0: Detect IaC flavor(s)
├── CloudFormation: AWSTemplateFormatVersion / Resources: top-level key
├── Terraform: .tf files, module blocks, terraform-provider-aws resources
├── Pulumi: @pulumi/aws (TS) or pulumi_aws (Python) imports
└── Crossplane: apiextensions.crossplane.io (XRD/Composition),
*.upbound.io / *.aws.crossplane.io (Managed Resources)
Phase 1: Inventory
├── Parse every resource declaration into (type, logical name, properties, references)
├── Resolve identifiers: literal values, variable defaults, tfvars files,
│ crossplane.io/external-name annotations (highest-fidelity source)
├── Mark unresolvable identifiers (computed values, runtime refs) for discovery
└── Detect composition units: TF modules, CFN nested stacks, Pulumi components,
Crossplane Composition + XRD pairs
Phase 2: Map to ACK
├── Map each IaC resource type to its ACK apiVersion/Kind
│ (references/iac-to-ack-mapping.md)
├── Determine adoption-fields requirement per Kind
│ (references/adoption-fields-ref.md)
└── Flag unsupported types (no GA ACK controller) for the report
Phase 3: Generate manifests
├── Flat resources -> ACK manifests with adopt-or-create + retain
├── Composition units -> kro ResourceGraphDefinition + instance CRs
│ (references/kro-patterns.md)
└── Unresolved identifiers -> TODO(discovery) comments + CLI commands
Phase 4: Validate
├── YAML syntax validation on every generated manifest
├── kubectl apply --dry-run=client (if kubectl available; needs CRDs - skip gracefully)
└── Annotation audit: 100% of generated manifests carry retain + adopt-or-create
Phase 5: Report
└── Generate ADOPTION_REPORT.md (inventory, skips, discovery, prerequisites,
apply order, post-adoption IaC decommission guidance)
Configuration
Options are provided via additionalPlanContext:
"Adopt all resources." - full repository scan
"Adopt only the networking module." - scope to a specific module/stack
"Skip kro, flat manifests only." - disable RGD generation
"Output directory: ack-adoption/" - override the default output location (ack-adoption/ at repo root)
Worked Examples
Example 1: Terraform S3 bucket -> ACK adoption manifest
Input (storage.tf):
resource "aws_s3_bucket" "reports" {
bucket = "acme-reports-prod"
tags = {
Team = "analytics"
}
}
Output (ack-adoption/s3-bucket-reports.yaml):
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
name: reports
annotations:
services.k8s.aws/adoption-policy: "adopt-or-create"
services.k8s.aws/deletion-policy: "retain"
spec:
name: acme-reports-prod
tagging:
tagSet:
- key: Team
value: analytics
Example 2: Terraform module -> kro ResourceGraphDefinition
Input (main.tf):
module "queue_with_dlq" {
source = "./modules/queue-with-dlq"
queue_name = "orders"
max_receive_count = 5
}
Where the module declares an aws_sqs_queue main queue plus an aws_sqs_queue DLQ wired via redrive_policy.
Output (ack-adoption/rgd-queue-with-dlq.yaml):
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: queue-with-dlq
spec:
schema:
apiVersion: v1alpha1
kind: QueueWithDLQ
spec:
queueName: string
maxReceiveCount: integer | default=5
resources:
- id: dlq
template:
apiVersion: sqs.services.k8s.aws/v1alpha1
kind: Queue
metadata:
name: ${schema.spec.queueName}-dlq
annotations:
services.k8s.aws/adoption-policy: "adopt-or-create"
services.k8s.aws/deletion-policy: "retain"
spec:
queueName: ${schema.spec.queueName}-dlq
- id: queue
template:
apiVersion: sqs.services.k8s.aws/v1alpha1
kind: Queue
metadata:
name: ${schema.spec.queueName}
annotations:
services.k8s.aws/adoption-policy: "adopt-or-create"
services.k8s.aws/deletion-policy: "retain"
spec:
queueName: ${schema.spec.queueName}
redrivePolicy: '{"deadLetterTargetArn":"${dlq.status.ackResourceMetadata.arn}","maxReceiveCount":${schema.spec.maxReceiveCount}}'
Plus the instance CR (ack-adoption/instance-orders-queue.yaml):
apiVersion: kro.run/v1alpha1
kind: QueueWithDLQ
metadata:
name: orders
spec:
queueName: orders
maxReceiveCount: 5
Full example set (CloudFormation nested stack -> RGD, VPC with adoption-fields, DynamoDB, IAM Role, unresolved-identifier TODO pattern) is in references/examples-iac-to-ack.md.
Reference Dispatch
Load reference files on demand based on what the scan finds:
| Signal |
Reference File |
| Any supported AWS resource type in the IaC |
references/iac-to-ack-mapping.md |
| A mapped Kind whose identifier lives in status (VPC, SQS, SNS, KMS, CloudFront, Route53, MSK, IAM Policy, EC2 network resources) |
references/adoption-fields-ref.md |
Terraform module block, CloudFormation AWS::CloudFormation::Stack, or Pulumi ComponentResource |
references/kro-patterns.md |
Any apiextensions.crossplane.io or *.upbound.io / *.aws.crossplane.io document |
references/crossplane-to-ack-kro.md (then kro-patterns.md for the RGD shape) |
| Any resource requiring a concrete before/after example |
references/examples-iac-to-ack.md |
Validation / Exit Criteria
- Every supported IaC resource has exactly one generated ACK manifest (or one kro RGD slot) - no duplicates, no silent drops.
- 100% of generated manifests (including templates inside RGDs) carry both
services.k8s.aws/adoption-policy: "adopt-or-create" and services.k8s.aws/deletion-policy: "retain".
- Every Kind requiring
adoption-fields either has the annotation populated with a resolved identifier, OR keeps the annotation fully commented out inside a TODO(discovery) block with the exact AWS CLI command and appears in the report's "Requires Discovery" section. No generated file contains an active adoption-fields annotation with a placeholder value.
- kro RGDs are generated only for genuine composition units; every RGD has at least one instance CR; internal references use CEL expressions, not IaC syntax remnants.
- No generated file contains IaC-native syntax (
!Ref, !GetAtt, ${var., ${module., Pulumi interpolations).
- All generated YAML parses cleanly;
kubectl apply --dry-run=client passes where CRDs are available (skipped gracefully otherwise).
- Source IaC files are byte-identical to their pre-run state.
ADOPTION_REPORT.md exists and contains: inventory table, skipped resources with reasons, discovery commands, prerequisite checklist (controller + feature gate + IAM permissions per service), dependency-sorted apply order, and post-adoption IaC decommission guidance.
Tips
ResourceAdoption feature gate is enabled by default when ACK controllers are installed via EKS Capabilities, but disabled by default on self-managed Helm installs - the prerequisite checklist in the report must state both paths explicitly.
- adopt-or-create reconciles the declared spec against AWS after adoption. A spec field that differs from the deployed resource triggers a real AWS update - fidelity between IaC properties and generated spec is a correctness requirement, not cosmetics.
- Terraform state files (
.tfstate) are the highest-fidelity identifier source when present in the repo - prefer state values over HCL interpolation resolution, but never require state to be present.
- CloudFormation
!Ref on a resource usually resolves to the physical ID at runtime - when the template alone cannot resolve it, the discovery command in the TODO should query by stack: aws cloudformation describe-stack-resources --stack-name <stack>.
- An empty or partial spec is safer than a wrong spec for adopt-or-create: prefer the minimal find+create field set and let ACK populate the rest from AWS state after adoption.
1---2name: ack-resource-adoption-from-iac3description: Analyzes CloudFormation, Terraform, Pulumi, and Crossplane code to generate ACK (AWS Controllers for Kubernetes) adoption manifests that bring existing AWS resources under Kubernetes/GitOps management without recreating them. Uses the ACK ResourceAdoption feature gate with adopt-or-create policy, and generates kro ResourceGraphDefinitions for reusable modules, stacked/nested components, and Crossplane Compositions. Trigger: ACK adoption, adopt-or-create, migrate IaC to Kubernetes, CloudFormation to ACK, Terraform to ACK, Crossplane to ACK, Composition to kro, GitOps migration, kro ResourceGraphDefinition.4---56# ACK Resource Adoption from IaC78## Objective910Convert existing IaC-managed AWS resources (CloudFormation, Terraform, Pulumi, Crossplane) into ACK adoption manifests so the resources come under Kubernetes/GitOps control **without being recreated, modified, or deleted**. Where the IaC uses reusable modules or nested/stacked structures, generate kro `ResourceGraphDefinition`s that preserve that composition as a first-class Kubernetes API.1112## Scope1314Analyzes:15- CloudFormation templates (`.yaml`, `.yml`, `.json` with `AWSTemplateFormatVersion` or `Resources:` top-level key), including nested stacks (`AWS::CloudFormation::Stack`)16- Terraform files (`.tf`, `.tf.json`), including local and registry modules (`module` blocks)17- Pulumi programs (TypeScript/Python with `@pulumi/aws` or `pulumi_aws` imports)18- Crossplane manifests: Managed Resources (`*.upbound.io`, `*.aws.crossplane.io`), `Composition` + `CompositeResourceDefinition` (`apiextensions.crossplane.io`), and claim instances (`references/crossplane-to-ack-kro.md`)1920Generates:21- One ACK adoption manifest per supported AWS resource, with `adoption-policy: adopt-or-create`, `deletion-policy: retain`, and `adoption-fields` when the identifier lives in status22- kro `ResourceGraphDefinition`s (`kro.run/v1alpha1`) for IaC modules/nested stacks that instantiate more than one resource, exposing module inputs as schema fields23- `ADOPTION_REPORT.md` documenting every mapped resource, every skipped resource (with reason), discovery commands, and the recommended apply order2425Supported ACK services (initial mapping table in `references/iac-to-ack-mapping.md`): EKS, EC2 (VPC, Subnet, SecurityGroup, RouteTable, InternetGateway), S3, DynamoDB, SQS, SNS, RDS, ElastiCache, IAM (Role, Policy), KMS, Lambda, ECR, MSK, OpenSearch, Secrets Manager, SSM, EventBridge, CloudFront, Route53.2627**Non-Goals** (out of scope for this transformation):281. Executing `kubectl apply` or touching any live cluster or AWS account - this transformation generates manifests and a report; the customer applies them following the documented order.292. Removing or altering the source IaC - the CloudFormation/Terraform/Pulumi code is read-only input. Decommissioning the old IaC (e.g., `terraform state rm`, CloudFormation stack deletion with retain policies) is documented as a manual post-adoption step in the report, never automated.303. Resources without a GA ACK controller - flagged in the report with the closest alternative (stay in IaC, or ACK controller roadmap link), never guessed.314. Installing ACK controllers or kro - prerequisites are documented in the report; installation is the customer's platform decision (EKS Capabilities vs Helm).3233## Constraints3435### Safety (non-negotiable)36- EVERY generated manifest MUST carry `services.k8s.aws/deletion-policy: "retain"`. A missing retain policy can cause real AWS resource deletion when a CR is removed - this is the single most dangerous failure mode of this transformation.37- EVERY generated manifest MUST use `services.k8s.aws/adoption-policy: "adopt-or-create"` so applying it never fails on an existing resource and never duplicates it.38- Never generate a manifest with placeholder/guessed identifiers. If the resource identifier cannot be resolved from the IaC (e.g., a Terraform computed value or a CloudFormation `!Ref` chain that requires runtime state), emit the manifest with an explicit `# TODO(discovery):` comment containing the exact AWS CLI discovery command, and list it in the report's "Requires Discovery" section.39- When an `adoption-fields` identifier is unresolved, the `services.k8s.aws/adoption-fields` annotation itself MUST stay commented out inside the `TODO(discovery)` block. Never emit an active `adoption-fields` annotation carrying a placeholder value (`TODO`, `REPLACE_ME`, or similar) - an active placeholder instructs ACK to adopt a resource by a fake identifier, which either fails adoption or matches the wrong AWS resource. The manifest stays inert for that resource until a human supplies the real value.40- The source IaC files are never modified.4142### Fidelity43- The generated `spec` must reflect the resource configuration declared in the IaC (adopt-or-create reconciles spec against AWS after adoption - a spec that diverges from reality causes unintended updates). When an IaC attribute has no ACK spec equivalent, document it in the report rather than dropping it silently.44- Preserve resource naming: the Kubernetes object name derives from the IaC logical name (kebab-cased), and the AWS-facing name field keeps the exact deployed value.45- Tags declared in IaC are carried into the ACK spec `tags` field when the controller supports it.4647### kro Generation Rules48- Generate a `ResourceGraphDefinition` ONLY when the IaC has a module/nested-stack instantiated as a unit (a Terraform `module` block, a CloudFormation nested stack, or a Pulumi ComponentResource). One-off flat resources get plain ACK manifests - do not wrap single resources in kro.49- Module input variables become kro `spec.schema.spec` fields with types and defaults mapped from the IaC variable definitions.50- Cross-resource references inside the module (e.g., subnet referencing VPC ID) become kro CEL expressions (`${resource.status.field}`), replacing the IaC-native references (`!Ref`, `!GetAtt`, Terraform interpolation).51- Each kro resource template inside the RGD still carries the adoption + retain annotations - adoption safety applies inside graphs exactly as it does for flat manifests.52- For module instances (multiple instantiations of the same module), generate ONE RGD plus one instance CR per instantiation, with the per-instance values.5354### Reporting55- `ADOPTION_REPORT.md` must contain: inventory table (IaC resource -> ACK Kind -> manifest file -> adoption-fields needed), skipped resources with reasons, discovery commands for unresolved identifiers, prerequisite checklist (controllers, feature gate, IRSA/Pod Identity permissions per service), recommended apply order (dependency-sorted: IAM/KMS -> network -> data stores -> compute), and post-adoption IaC decommission guidance.56- Every generated file must be listed with its source IaC file and line reference.5758## Workflow5960```text61Phase 0: Detect IaC flavor(s)62 ├── CloudFormation: AWSTemplateFormatVersion / Resources: top-level key63 ├── Terraform: .tf files, module blocks, terraform-provider-aws resources64 ├── Pulumi: @pulumi/aws (TS) or pulumi_aws (Python) imports65 └── Crossplane: apiextensions.crossplane.io (XRD/Composition),66 *.upbound.io / *.aws.crossplane.io (Managed Resources)6768Phase 1: Inventory69 ├── Parse every resource declaration into (type, logical name, properties, references)70 ├── Resolve identifiers: literal values, variable defaults, tfvars files,71 │ crossplane.io/external-name annotations (highest-fidelity source)72 ├── Mark unresolvable identifiers (computed values, runtime refs) for discovery73 └── Detect composition units: TF modules, CFN nested stacks, Pulumi components,74 Crossplane Composition + XRD pairs7576Phase 2: Map to ACK77 ├── Map each IaC resource type to its ACK apiVersion/Kind78 │ (references/iac-to-ack-mapping.md)79 ├── Determine adoption-fields requirement per Kind80 │ (references/adoption-fields-ref.md)81 └── Flag unsupported types (no GA ACK controller) for the report8283Phase 3: Generate manifests84 ├── Flat resources -> ACK manifests with adopt-or-create + retain85 ├── Composition units -> kro ResourceGraphDefinition + instance CRs86 │ (references/kro-patterns.md)87 └── Unresolved identifiers -> TODO(discovery) comments + CLI commands8889Phase 4: Validate90 ├── YAML syntax validation on every generated manifest91 ├── kubectl apply --dry-run=client (if kubectl available; needs CRDs - skip gracefully)92 └── Annotation audit: 100% of generated manifests carry retain + adopt-or-create9394Phase 5: Report95 └── Generate ADOPTION_REPORT.md (inventory, skips, discovery, prerequisites,96 apply order, post-adoption IaC decommission guidance)97```9899### Configuration100101Options are provided via `additionalPlanContext`:102- `"Adopt all resources."` - full repository scan103- `"Adopt only the networking module."` - scope to a specific module/stack104- `"Skip kro, flat manifests only."` - disable RGD generation105- `"Output directory: ack-adoption/"` - override the default output location (`ack-adoption/` at repo root)106107## Worked Examples108109### Example 1: Terraform S3 bucket -> ACK adoption manifest110111**Input (`storage.tf`):**112```hcl113resource "aws_s3_bucket" "reports" {114 bucket = "acme-reports-prod"115 tags = {116 Team = "analytics"117 }118}119```120121**Output (`ack-adoption/s3-bucket-reports.yaml`):**122```yaml123apiVersion: s3.services.k8s.aws/v1alpha1124kind: Bucket125metadata:126 name: reports127 annotations:128 services.k8s.aws/adoption-policy: "adopt-or-create"129 services.k8s.aws/deletion-policy: "retain"130spec:131 name: acme-reports-prod132 tagging:133 tagSet:134 - key: Team135 value: analytics136```137138### Example 2: Terraform module -> kro ResourceGraphDefinition139140**Input (`main.tf`):**141```hcl142module "queue_with_dlq" {143 source = "./modules/queue-with-dlq"144 queue_name = "orders"145 max_receive_count = 5146}147```148149Where the module declares an `aws_sqs_queue` main queue plus an `aws_sqs_queue` DLQ wired via `redrive_policy`.150151**Output (`ack-adoption/rgd-queue-with-dlq.yaml`):**152```yaml153apiVersion: kro.run/v1alpha1154kind: ResourceGraphDefinition155metadata:156 name: queue-with-dlq157spec:158 schema:159 apiVersion: v1alpha1160 kind: QueueWithDLQ161 spec:162 queueName: string163 maxReceiveCount: integer | default=5164 resources:165 - id: dlq166 template:167 apiVersion: sqs.services.k8s.aws/v1alpha1168 kind: Queue169 metadata:170 name: ${schema.spec.queueName}-dlq171 annotations:172 services.k8s.aws/adoption-policy: "adopt-or-create"173 services.k8s.aws/deletion-policy: "retain"174 spec:175 queueName: ${schema.spec.queueName}-dlq176 - id: queue177 template:178 apiVersion: sqs.services.k8s.aws/v1alpha1179 kind: Queue180 metadata:181 name: ${schema.spec.queueName}182 annotations:183 services.k8s.aws/adoption-policy: "adopt-or-create"184 services.k8s.aws/deletion-policy: "retain"185 spec:186 queueName: ${schema.spec.queueName}187 redrivePolicy: '{"deadLetterTargetArn":"${dlq.status.ackResourceMetadata.arn}","maxReceiveCount":${schema.spec.maxReceiveCount}}'188```189190**Plus the instance CR (`ack-adoption/instance-orders-queue.yaml`):**191```yaml192apiVersion: kro.run/v1alpha1193kind: QueueWithDLQ194metadata:195 name: orders196spec:197 queueName: orders198 maxReceiveCount: 5199```200201Full example set (CloudFormation nested stack -> RGD, VPC with adoption-fields, DynamoDB, IAM Role, unresolved-identifier TODO pattern) is in `references/examples-iac-to-ack.md`.202203## Reference Dispatch204205Load reference files on demand based on what the scan finds:206207| Signal | Reference File |208|---|---|209| Any supported AWS resource type in the IaC | `references/iac-to-ack-mapping.md` |210| A mapped Kind whose identifier lives in status (VPC, SQS, SNS, KMS, CloudFront, Route53, MSK, IAM Policy, EC2 network resources) | `references/adoption-fields-ref.md` |211| Terraform `module` block, CloudFormation `AWS::CloudFormation::Stack`, or Pulumi ComponentResource | `references/kro-patterns.md` |212| Any `apiextensions.crossplane.io` or `*.upbound.io` / `*.aws.crossplane.io` document | `references/crossplane-to-ack-kro.md` (then `kro-patterns.md` for the RGD shape) |213| Any resource requiring a concrete before/after example | `references/examples-iac-to-ack.md` |214215## Validation / Exit Criteria2162171. Every supported IaC resource has exactly one generated ACK manifest (or one kro RGD slot) - no duplicates, no silent drops.2182. 100% of generated manifests (including templates inside RGDs) carry both `services.k8s.aws/adoption-policy: "adopt-or-create"` and `services.k8s.aws/deletion-policy: "retain"`.2193. Every Kind requiring `adoption-fields` either has the annotation populated with a resolved identifier, OR keeps the annotation fully commented out inside a `TODO(discovery)` block with the exact AWS CLI command and appears in the report's "Requires Discovery" section. No generated file contains an active `adoption-fields` annotation with a placeholder value.2204. kro RGDs are generated only for genuine composition units; every RGD has at least one instance CR; internal references use CEL expressions, not IaC syntax remnants.2215. No generated file contains IaC-native syntax (`!Ref`, `!GetAtt`, `${var.`, `${module.`, Pulumi interpolations).2226. All generated YAML parses cleanly; `kubectl apply --dry-run=client` passes where CRDs are available (skipped gracefully otherwise).2237. Source IaC files are byte-identical to their pre-run state.2248. `ADOPTION_REPORT.md` exists and contains: inventory table, skipped resources with reasons, discovery commands, prerequisite checklist (controller + feature gate + IAM permissions per service), dependency-sorted apply order, and post-adoption IaC decommission guidance.225226## Tips227228- `ResourceAdoption` feature gate is enabled by default when ACK controllers are installed via EKS Capabilities, but disabled by default on self-managed Helm installs - the prerequisite checklist in the report must state both paths explicitly.229- adopt-or-create reconciles the declared spec against AWS after adoption. A spec field that differs from the deployed resource triggers a real AWS update - fidelity between IaC properties and generated spec is a correctness requirement, not cosmetics.230- Terraform state files (`.tfstate`) are the highest-fidelity identifier source when present in the repo - prefer state values over HCL interpolation resolution, but never require state to be present.231- CloudFormation `!Ref` on a resource usually resolves to the physical ID at runtime - when the template alone cannot resolve it, the discovery command in the TODO should query by stack: `aws cloudformation describe-stack-resources --stack-name <stack>`.232- An empty or partial spec is safer than a wrong spec for adopt-or-create: prefer the minimal find+create field set and let ACK populate the rest from AWS state after adoption.