<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pranit Raje</title>
    <description>The latest articles on DEV Community by Pranit Raje (@pranitraje).</description>
    <link>https://dev.to/pranitraje</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F22979%2Fd0ef9151-94a6-4bb4-b512-c2514a0092e3.jpeg</url>
      <title>DEV Community: Pranit Raje</title>
      <link>https://dev.to/pranitraje</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranitraje"/>
    <language>en</language>
    <item>
      <title>How to Import Existing AWS Infrastructure into Terraform Without Downtime</title>
      <dc:creator>Pranit Raje</dc:creator>
      <pubDate>Wed, 07 Oct 2026 07:11:11 +0000</pubDate>
      <link>https://dev.to/pranitraje/how-to-import-existing-aws-infrastructure-into-terraform-without-downtime-1h17</link>
      <guid>https://dev.to/pranitraje/how-to-import-existing-aws-infrastructure-into-terraform-without-downtime-1h17</guid>
      <description>&lt;h2&gt;
  
  
  Bringing a live three-tier app under Terraform control with Terraform Search, HCP Terraform's Search and Import, Terraform MCP Server, HashiCorp Agent Skills and Sentinel policy that stopped a plan from replacing my EKS cluster
&lt;/h2&gt;




&lt;p&gt;Most organisations have at least one of these: an application that is genuinely important, genuinely in production, and genuinely not in code. Nobody planned it that way. Someone spun it up in the console, a team started depending on it, then a customer did, and now there is a quiet understanding that nobody touches it unless something is on fire.&lt;/p&gt;

&lt;p&gt;This is the real IaC adoption problem. Greenfield is easy. The hard part is the brownfield stuff that already works and therefore cannot be rebuilt just so it can be described properly.&lt;/p&gt;

&lt;p&gt;Terraform has had an answer for years — &lt;code&gt;terraform import&lt;/code&gt;, and later the declarative &lt;code&gt;import&lt;/code&gt; block. Both work. Both are a grind at any real scale. One &lt;code&gt;import&lt;/code&gt; block per resource, hand-written config to match whatever is actually deployed, then &lt;code&gt;plan&lt;/code&gt;, read the diff, fix a field, &lt;code&gt;plan&lt;/code&gt; again. For a forty-resource app that is a tense sprint, because every &lt;code&gt;plan&lt;/code&gt; against a live system is a small act of faith.&lt;/p&gt;

&lt;p&gt;So when Terraform shipped &lt;strong&gt;Search&lt;/strong&gt; (&lt;code&gt;list&lt;/code&gt; blocks in &lt;code&gt;.tfquery.hcl&lt;/code&gt;) and HCP Terraform shipped &lt;strong&gt;Search and Import&lt;/strong&gt;, I wanted to know whether the economics had actually changed. Not on a toy S3 bucket — on something with the shape of a real app: load balancer in front, compute in the middle, database at the back, and all the networking and IAM plumbing holding it together.&lt;/p&gt;




&lt;blockquote&gt;
&lt;h3&gt;
  
  
  A note on what this is
&lt;/h3&gt;

&lt;p&gt;This was a demo I built for a few clients while working at HashiCorp, to show what HCP Terraform, Search and Import, the Terraform MCP Server, and HashiCorp's official Agent Skills can do together. It is &lt;strong&gt;not production-ready&lt;/strong&gt;, and there is plenty of room to improve it.&lt;/p&gt;

&lt;p&gt;Every environment is different. Account layouts, tagging discipline, naming conventions, and blast-radius tolerance all vary. Take the learnings here, not the artefacts, and adapt them — you will likely find better approaches for your own setup. Search and Import is also still an &lt;strong&gt;experimental&lt;/strong&gt; feature as of this date, which is worth factoring into how much you lean on it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Everything below is one box in this picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonjsyhxu69dea01j58vf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonjsyhxu69dea01j58vf.png" alt="High-level Terraform import workflow" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Search&lt;/strong&gt; does discovery. I declare what I am looking for instead of clicking through the console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Terraform MCP Server and HashiCorp's Terraform Agent Skills&lt;/strong&gt; do the authoring. The assistant writes the query file and later fixes the generated config, but every argument comes from the official registry docs rather than from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HCP Terraform's Search and Import&lt;/strong&gt; turns discovered resources into a starter configuration plus matching &lt;code&gt;import&lt;/code&gt; blocks, in bulk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel policies&lt;/strong&gt; sit on the workspace so no run can quietly turn an import into a rebuild.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth one is the reason this was safe enough to attempt at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The application
&lt;/h2&gt;

&lt;p&gt;Deliberately boring, because the interesting part is the import workflow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;AWS services&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Front end&lt;/td&gt;
&lt;td&gt;Application Load Balancer, target group, HTTP listener&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web / app&lt;/td&gt;
&lt;td&gt;EKS cluster and managed node group, launch template, IAM roles, IRSA OIDC provider, ECR repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;DynamoDB table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Foundation&lt;/td&gt;
&lt;td&gt;VPC, 4 subnets across 2 AZs, internet gateway, NAT gateway, EIP, 2 route tables, routes, subnet associations, 3 security groups and their ingress rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small Flask app. User fills in a form, the app writes it to DynamoDB, a list view reads it back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoeryt3fefbbmrc1bun1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoeryt3fefbbmrc1bun1.png" alt="The demo application UI running on EKS" width="800" height="884"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That screenshot matters more than it looks like it should. Through everything that follows, this page stayed up. That was the actual success criterion — not "did &lt;code&gt;apply&lt;/code&gt; exit zero" but "did the thing I imported keep serving users while I imported it".&lt;/p&gt;

&lt;p&gt;I provisioned the stack with a &lt;strong&gt;CloudFormation template&lt;/strong&gt;, purely to create the brownfield condition. It could equally have been a console session, a Python script, or a runbook full of &lt;code&gt;aws&lt;/code&gt; CLI commands. The only property I needed was that Terraform had never seen it and had no state for it.&lt;/p&gt;

&lt;p&gt;The one convenience CFN gave me is that it tags everything with &lt;code&gt;aws:cloudformation:stack-name&lt;/code&gt;, which made a clean discovery handle. If your brownfield app has no such tag, you will lean harder on names, VPC IDs, and parent-resource scoping — which I had to do for plenty of resources anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Discovery as code
&lt;/h2&gt;

&lt;p&gt;Terraform Search works through &lt;code&gt;list&lt;/code&gt; blocks. Each one says "find resources of this type matching this filter". You run &lt;code&gt;terraform query&lt;/code&gt;, and Terraform returns what it found, including the identity attributes needed to import each resource.&lt;/p&gt;

&lt;p&gt;Small feature, but it changes the character of the work. Discovery stops being archaeology you do by hand and becomes a file you can commit, review, and re-run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before you write anything, check that your resource types are actually supported.&lt;/strong&gt; List resource coverage is per-provider and per-version, and it is growing fast, so anything written down about it goes stale quickly. The reference I use every time is &lt;a href="https://github.com/quixoticmonk/extract-provider-supported-actions/blob/main/list-resources-report.json" rel="noopener noreferrer"&gt;this auto-generated report&lt;/a&gt;, which tracks supported list resources and refreshes as providers ship updates. Cheaper than discovering mid-build that the resource you care about has no &lt;code&gt;list&lt;/code&gt; support yet. You can also query your own provider schema directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform providers schema &lt;span class="nt"&gt;-json&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="s1"&gt;'.provider_schemas | to_entries | map({provider: .key, lists: (.value.list_resource_schemas // {} | keys)})'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing the query file is fiddly, though. Twenty-three resource types, each with its own filter semantics, and the argument names are not guessable. &lt;code&gt;aws_vpc&lt;/code&gt; takes an EC2-style &lt;code&gt;filter { name = "tag:..." }&lt;/code&gt;. &lt;code&gt;aws_launch_template&lt;/code&gt; takes &lt;code&gt;launch_template_names&lt;/code&gt;. &lt;code&gt;aws_eks_node_group&lt;/code&gt; takes &lt;code&gt;cluster_name&lt;/code&gt;. &lt;code&gt;aws_iam_role&lt;/code&gt; takes nothing and returns every non-service-linked role in the account.&lt;/p&gt;

&lt;p&gt;Exactly the kind of work where an AI assistant helps, and exactly the kind where it will confidently invent an argument that does not exist. So I constrained it with two things: the &lt;strong&gt;&lt;code&gt;terraform-search-import&lt;/code&gt; Agent Skill&lt;/strong&gt; (Search conventions and workflow) and the &lt;strong&gt;Terraform MCP Server&lt;/strong&gt; (live access to registry docs). Then I made the lookup non-optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Process&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Inventory**&lt;/span&gt;: Parse &lt;span class="sb"&gt;`three-tier-app.yaml`&lt;/span&gt; and list every resource
   (logical ID + &lt;span class="sb"&gt;`Type`&lt;/span&gt;).
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Map each resource to a list resource**&lt;/span&gt;:
   a. Map each CloudFormation type to its Terraform resource type in &lt;span class="sb"&gt;`hashicorp/aws`&lt;/span&gt;.
   b. Use the Terraform MCP server to check whether the latest &lt;span class="sb"&gt;`hashicorp/aws`&lt;/span&gt;
      provider offers a &lt;span class="gs"&gt;**list resource**&lt;/span&gt; for that type. Check the registry docs;
      don't rely on memory.
   c. If, and only if, &lt;span class="sb"&gt;`aws`&lt;/span&gt; has no list resource for it, check &lt;span class="sb"&gt;`hashicorp/awscc`&lt;/span&gt;.
   d. If neither provider supports it, don't invent one. Record it as unsupported.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Filter by tag**&lt;/span&gt;: If a list resource can't filter by tag, use the narrowest
   documented alternative (e.g., VPC ID, name prefix, parent resource).
   Add an HCL comment that explains why you used it.

&lt;span class="gh"&gt;# Constraints&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Every list resource and argument must exist in the registry docs you looked up.
  Never guess.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three instructions did most of the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Don't rely on memory."&lt;/strong&gt; On an earlier attempt, before I was this explicit, one filter argument came back from what was effectively a general web search. It looked plausible. It was wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Don't invent one. Record it as unsupported."&lt;/strong&gt; List resource coverage is expanding fast but incomplete. In the end there was one gap: &lt;code&gt;AWS::EC2::VPCGatewayAttachment&lt;/code&gt; has no &lt;code&gt;aws&lt;/code&gt; list resource, so it fell back to &lt;code&gt;awscc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Add an HCL comment that explains why."&lt;/strong&gt; The most valuable line in the prompt. The query file now documents its own compromises.&lt;/p&gt;

&lt;p&gt;Clean, tag-filtered discovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"stack_vpcs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;
  &lt;span class="nx"&gt;include_resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tag:aws:cloudformation:stack-name"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the compromises, which are the more honest half of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Route resources cannot be tagged; discover routes beneath the tagged tables.&lt;/span&gt;
&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="s2"&gt;"aws_route"&lt;/span&gt; &lt;span class="s2"&gt;"routes_in_stack_route_tables"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack_route_tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;
  &lt;span class="nx"&gt;include_resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# The EKS cluster list resource has no filter arguments and lists clusters region-wide.&lt;/span&gt;
&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"eks_clusters"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;
  &lt;span class="nx"&gt;include_resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you take one piece of syntax from this post, take the &lt;code&gt;for_each&lt;/code&gt; in that first block. Child resources — routes, route table associations, security group rules, listeners — generally cannot be tagged, so you cannot find them by tag. But you can find their tagged parents and pivot: feed the parents' IDs into a &lt;code&gt;for_each&lt;/code&gt; on the child's &lt;code&gt;list&lt;/code&gt; block. &lt;code&gt;list.aws_route_table.stack_route_tables.data&lt;/code&gt; is the output of one query used as input to the next. Discovery composes.&lt;/p&gt;

&lt;p&gt;That pivot is also why &lt;code&gt;include_resource = true&lt;/code&gt; appears on every block. By default a query returns only resource &lt;em&gt;identities&lt;/em&gt;. Set it to &lt;code&gt;true&lt;/code&gt; and the provider returns full resource objects, which is what makes &lt;code&gt;table.state.id&lt;/code&gt; readable by the next query. Without it the &lt;code&gt;for_each&lt;/code&gt; has nothing to pivot on. The &lt;a href="https://developer.hashicorp.com/terraform/language/import/bulk#include_resource" rel="noopener noreferrer"&gt;docs note&lt;/a&gt; it may affect performance, so it is worth being deliberate about rather than setting everywhere out of habit — though for a few dozen resources I never noticed the cost.&lt;/p&gt;

&lt;p&gt;The resources with no filters at all (EKS clusters, load balancers, DynamoDB tables, ECR repositories, IAM roles) return everything in the region or account. Fine in a demo account. Noisy in a busy shared one — a real limitation, and one more reason the phased approach below is worth the extra runs.&lt;/p&gt;

&lt;p&gt;Twenty-three &lt;code&gt;list&lt;/code&gt; blocks. One &lt;code&gt;terraform query&lt;/code&gt;. Every resource found, with its import identity attached.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: From discovery to configuration
&lt;/h2&gt;

&lt;p&gt;With the query file validated, I moved to HCP Terraform's &lt;strong&gt;Search and Import&lt;/strong&gt;. Point it at the query, it runs discovery, you tick the resources you want, and it generates a starter configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0s4g7xkywj13cnjc92um.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0s4g7xkywj13cnjc92um.png" alt="Selecting discovered resources and generating a starter configuration in HCP Terraform" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Have your resource IDs ready before you open this screen
&lt;/h3&gt;

&lt;p&gt;This is the prerequisite nobody tells you about, and it caught me out on my first attempt.&lt;/p&gt;

&lt;p&gt;Search and Import returns &lt;strong&gt;everything&lt;/strong&gt; your query matched — resources already managed by Terraform alongside the unmanaged ones you are trying to adopt. It is a discovery tool, not a filter. Selecting the right rows is your job.&lt;/p&gt;

&lt;p&gt;Say you want to import your unmanaged security groups. Your &lt;code&gt;list&lt;/code&gt; block matches on the VPC or a tag, and you get back a list of every security group in scope. Some are already in state. Some belong to a different team. Some are the ones you actually want. To tell them apart on that screen you need the &lt;strong&gt;exact security group IDs&lt;/strong&gt; of your targets in hand — &lt;code&gt;sg-0bd9f4d46a38xxxxx&lt;/code&gt;, not "the one for the EKS nodes".&lt;/p&gt;

&lt;p&gt;Two things follow from that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build your inventory first.&lt;/strong&gt; Before you touch the UI, have a list of the resources you intend to import and their unique identifiers. Console, &lt;code&gt;aws&lt;/code&gt; CLI, a tagging report, your CMDB — however you get there, get there before you start selecting. Trying to assemble that list while staring at a few hundred discovered rows is how you import something that belongs to somebody else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know which identifier each resource type uses&lt;/strong&gt;, because it is not consistent. A security group is identified by &lt;code&gt;id&lt;/code&gt;, an EKS cluster by &lt;code&gt;name&lt;/code&gt;, a load balancer or IAM policy by &lt;code&gt;arn&lt;/code&gt;, an EKS node group by &lt;code&gt;cluster_name&lt;/code&gt; &lt;em&gt;plus&lt;/em&gt; &lt;code&gt;node_group_name&lt;/code&gt;. The identity schema is documented per resource in the Terraform Registry — check it there rather than assuming, which is another spot where having the MCP Server wired in paid off. You can see the shape of it in the generated &lt;code&gt;import&lt;/code&gt; blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Security group — identified by id&lt;/span&gt;
&lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1243XXXXXXXX"&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sg-0bd9f4d46a38XXXXX"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# EKS node group — composite identity, no id at all&lt;/span&gt;
&lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1243XXXXXXXX"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"demo-eks-cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;node_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"demo-eks-nodegroup"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets worse the busier your account is. In a demo account with one stack, the discovered list is short and obvious. In a shared production account where &lt;code&gt;list "aws_iam_role"&lt;/code&gt; returns every role you own, the identifiers are the only thing standing between you and adopting a resource another team depends on.&lt;/p&gt;

&lt;p&gt;The same thing works from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform query                                  &lt;span class="c"&gt;# discover&lt;/span&gt;
terraform query &lt;span class="nt"&gt;-generate-config-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;main.tf     &lt;span class="c"&gt;# generate resource + import blocks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each resource you get a &lt;code&gt;resource&lt;/code&gt; block populated from the live API response, plus an &lt;code&gt;import&lt;/code&gt; block keyed on identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack_vpcs_0&lt;/span&gt;
  &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;account_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1243XXXXXXXX"&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-037c4dd02c90XXXXX"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast-1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"stack_vpcs_0"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;                           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_network_address_usage_metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;instance_tenancy&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;                               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast-1"&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"demo-vpc"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously I would have written that &lt;code&gt;import&lt;/code&gt; block by hand, looked up the ID, hand-written the &lt;code&gt;resource&lt;/code&gt; block, and iterated on &lt;code&gt;plan&lt;/code&gt; until the diff was empty — forty times. Here, forty resources' worth of scaffolding arrived at once with correct identities.&lt;/p&gt;

&lt;p&gt;That is the real win, and I want to state it plainly: &lt;strong&gt;Search and Import does not give you production-ready Terraform. It gives you a very good head start on the mechanical half of the job.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: "Starter configuration" means exactly that
&lt;/h2&gt;

&lt;p&gt;HCP Terraform is upfront about it. The feature is experimental, and the console tells you to &lt;em&gt;review and modify this configuration before applying&lt;/em&gt;. Having worked through it, that label is precise.&lt;/p&gt;

&lt;p&gt;The reason is structural. The generator populates config from everything the API returned, but a provider schema mixes three kinds of fields: things you set, things the API computes, and things that are mutually exclusive with other things. The API response does not distinguish between them. So you get attributes that cannot legally be set, pairs of conflicting arguments, and a lot of zero-valued noise.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;terraform validate&lt;/code&gt; on the raw file produced a long list of errors. The patterns repeat, so they are worth naming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only attributes written as inputs.&lt;/strong&gt; &lt;code&gt;tags_all&lt;/code&gt; is the classic — computed from &lt;code&gt;tags&lt;/code&gt; plus provider default tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;= null&lt;/code&gt; and zero-value noise.&lt;/strong&gt; &lt;code&gt;disk_size = 0&lt;/code&gt;, &lt;code&gt;read_capacity = 0&lt;/code&gt; on a &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt; DynamoDB table, empty &lt;code&gt;timeouts&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutually exclusive arguments both set.&lt;/strong&gt; The biggest category. &lt;code&gt;launch_template.id&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;launch_template.name&lt;/code&gt;. &lt;code&gt;max_unavailable&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;max_unavailable_percentage&lt;/code&gt;. &lt;code&gt;subnets&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;subnet_mapping&lt;/code&gt; on an LB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Values outside valid ranges.&lt;/strong&gt; &lt;code&gt;order = 0&lt;/code&gt; on a listener action, where the provider needs 1 or higher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline vs standalone conflicts.&lt;/strong&gt; Subtle and important: the generator emitted inline &lt;code&gt;ingress&lt;/code&gt;/&lt;code&gt;egress&lt;/code&gt; blocks on &lt;code&gt;aws_security_group&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; standalone &lt;code&gt;aws_vpc_security_group_ingress_rule&lt;/code&gt; resources for the same rules, because the query discovered both. Those cannot coexist. Same story with inline &lt;code&gt;route&lt;/code&gt; blocks vs &lt;code&gt;aws_route&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant &lt;code&gt;provider = aws&lt;/code&gt; lines&lt;/strong&gt;, which &lt;code&gt;validate&lt;/code&gt; rejects inside &lt;code&gt;import&lt;/code&gt; blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than fix 1,300 lines by hand, I pointed the same MCP-grounded setup at the generated file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Objective&lt;/span&gt;
Find and fix broken or invalid code in &lt;span class="sb"&gt;`main.tf`&lt;/span&gt;. Keep the intended
infrastructure exactly as it is.

&lt;span class="gh"&gt;# Process&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Baseline**&lt;/span&gt;: Run &lt;span class="sb"&gt;`terraform validate`&lt;/span&gt;. Record every error and warning.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Documentation review**&lt;/span&gt;: Use the Terraform MCP server to check that every
   argument is valid in the schema, that conflicting arguments aren't set
   together (&lt;span class="sb"&gt;`ConflictsWith`&lt;/span&gt; / &lt;span class="sb"&gt;`ExactlyOneOf`&lt;/span&gt;), and that read-only attributes
   are not being set.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Classify**&lt;/span&gt; each finding as Breaking or Warning, based on a specific doc
   page or validate error. Never guess.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Fix**&lt;/span&gt;: smallest change that works. If a fix could force a resource to be
   replaced, or the right value is unclear, stop and ask.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Re-validate**&lt;/span&gt; and repeat until validate passes or the rest needs my input.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two instructions I would not drop: &lt;strong&gt;"keep the intended infrastructure exactly as it is"&lt;/strong&gt; and &lt;strong&gt;"if a fix could force a replacement, stop and ask."&lt;/strong&gt; An assistant optimising for a clean &lt;code&gt;validate&lt;/code&gt; will happily delete an argument to make an error go away. During a brownfield import, deleting an argument is how you end up with a plan that wants to replace your production node group. Correctness here is not "validate passes" — it is "validate passes &lt;em&gt;and&lt;/em&gt; the plan is a pure import".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;terraform validate&lt;/code&gt; only checks schema and syntax. It cannot tell you whether your plan is about to replace a live resource. A clean &lt;code&gt;validate&lt;/code&gt; is the start of the review, not the end of it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I needed something that &lt;em&gt;could&lt;/em&gt; tell me that, on every run, before anything was applied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Guardrails first, imports second
&lt;/h2&gt;

&lt;p&gt;I want to be emphatic about the ordering, because it is the most transferable idea here.&lt;/p&gt;

&lt;p&gt;I attached the policies &lt;strong&gt;before&lt;/strong&gt; I imported anything.&lt;/p&gt;

&lt;p&gt;The failure mode of a brownfield import is not subtle, but it is quiet. You misread one attribute. That attribute turns out to be &lt;code&gt;ForceNew&lt;/code&gt;. Your plan, which you believed was an import, now contains a &lt;code&gt;replace&lt;/code&gt;. Reading a forty-resource plan at the end of a long day, you can absolutely miss that — and the blast radius is your running application.&lt;/p&gt;

&lt;p&gt;That is a textbook policy-as-code problem: a specific, machine-checkable invariant that must hold on every run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy one: no destructive changes (hard-mandatory)
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;An import run must not destroy or replace anything.&lt;/em&gt; In plan terms, three action sets are forbidden: &lt;code&gt;["delete"]&lt;/code&gt;, &lt;code&gt;["delete","create"]&lt;/code&gt;, and &lt;code&gt;["create","delete"]&lt;/code&gt;. Everything else — imports, no-ops, updates — is fine as far as this policy is concerned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tfplan/v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tfplan&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tfrun&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Skip enforcement for explicit destroy runs.
&lt;/span&gt;&lt;span class="n"&gt;is_destroy_run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tfrun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_destroy&lt;/span&gt;

&lt;span class="n"&gt;destructive_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;destructive_changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;tfplan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_changes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_destroy_run&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt; &lt;span class="n"&gt;destructive_actions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;da&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;actions_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;da&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;is_destroy_run&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;violation_count&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth copying. The &lt;code&gt;tfrun.is_destroy&lt;/code&gt; escape hatch means &lt;code&gt;terraform destroy&lt;/code&gt; still works when you genuinely mean it — a guardrail that blocks legitimate operations gets switched off, and a switched-off guardrail protects nothing. And the policy prints the address, type, and action of every offending resource, so a failure tells you &lt;em&gt;why&lt;/em&gt; without hunting.&lt;/p&gt;

&lt;p&gt;This runs at &lt;strong&gt;hard-mandatory&lt;/strong&gt;. No override. I cannot talk myself past it at 7pm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy two: flag in-place updates (soft-mandatory)
&lt;/h3&gt;

&lt;p&gt;A correct import produces a plan full of imports and nothing else. If the plan also wants to &lt;code&gt;update&lt;/code&gt; something, that is not dangerous but it is always informative — it means my generated config does not exactly match what is deployed. Either the generator got a field slightly wrong, or there is real drift.&lt;/p&gt;

&lt;p&gt;I do not want to block that. I want to be told and then decide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;updated_resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;tfplan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_changes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;any&lt;/span&gt; &lt;span class="n"&gt;update_actions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;actions_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;update_count&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="s2"&gt;"no-destructive-changes"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./policies/no-destructive-changes.sentinel"&lt;/span&gt;
  &lt;span class="nx"&gt;enforcement_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hard-mandatory"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="s2"&gt;"flag-resource-updates"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./policies/flag-resource-updates.sentinel"&lt;/span&gt;
  &lt;span class="nx"&gt;enforcement_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"soft-mandatory"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two policies, about 120 lines of Sentinel. That is the entire safety apparatus.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Import in phases
&lt;/h2&gt;

&lt;p&gt;The temptation, once you have a working query file and generated config, is to apply all forty resources in one run. I did not, and I would push back on anyone who wanted to. I imported in dependency order, validating each layer before starting the next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Networking&lt;/strong&gt; — VPC, subnets, gateways, route tables, routes, associations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM&lt;/strong&gt; — roles, inline policies, customer-managed policies, OIDC provider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security groups&lt;/strong&gt; — and their ingress rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute&lt;/strong&gt; — launch template, EKS cluster, node group&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing&lt;/strong&gt; — target group, ALB, listener&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt; — DynamoDB table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry&lt;/strong&gt; — ECR repository&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Networking:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78uadd6skxjir493v05h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78uadd6skxjir493v05h.png" alt="VPC resource import plan in HCP Terraform" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;IAM:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluwsxheszbx7k8hplof1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluwsxheszbx7k8hplof1.png" alt="IAM resource import plan in HCP Terraform" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compute:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fow3b2capdpoydrmmijqk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fow3b2capdpoydrmmijqk.png" alt="EKS resource import plan in HCP Terraform" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why this is worth the extra runs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius is one layer.&lt;/strong&gt; If something looks wrong in the security group phase, there are three resources to inspect, not forty across seven services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependencies resolve naturally.&lt;/strong&gt; The EKS cluster references subnet IDs, security group IDs, and a role ARN. By the time I reach compute, those are already in state and verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plans stay readable.&lt;/strong&gt; This is the one I care about most. A plan with five imports is a plan I will actually read. A plan with forty is a plan I will scroll past while telling myself it looks fine. The policies exist because I do not fully trust myself on that — but the best way to use a guardrail is to not need it, and small plans are how you get there.&lt;/p&gt;

&lt;p&gt;The full sequence in the workspace history, each entry one layer, reviewed and applied on its own:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fooqrjkyyh39h9m5excmv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fooqrjkyyh39h9m5excmv.png" alt="Full Terraform run history for the phased import" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the guardrails earned their keep
&lt;/h2&gt;

&lt;p&gt;They fired twice. Both times they were right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one that would have cost me my EKS cluster
&lt;/h3&gt;

&lt;p&gt;The starter configuration for my cluster came back with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"eks_clusters_0"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bootstrap_self_managed_addons&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One boolean, generated for me, with a value that looks like a sensible default. But &lt;code&gt;bootstrap_self_managed_addons&lt;/code&gt; is &lt;code&gt;ForceNew&lt;/code&gt; on &lt;code&gt;aws_eks_cluster&lt;/code&gt;, because it describes a decision made at creation time. My live cluster was created with it &lt;code&gt;true&lt;/code&gt;. A mismatch on that field does not produce an update — it produces a &lt;strong&gt;replacement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So a plan I thought was importing my EKS cluster was proposing to destroy and recreate the cluster that was, at that moment, serving the app in the screenshot at the top of this post.&lt;/p&gt;

&lt;p&gt;The hard-mandatory policy caught it and failed the run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfnv32blxbyfvdcu04bi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfnv32blxbyfvdcu04bi.png" alt="Hard-mandatory Sentinel policy blocking a destructive EKS change" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the whole argument, so it is worth sitting with. I &lt;em&gt;did&lt;/em&gt; review the config. &lt;code&gt;terraform validate&lt;/code&gt; passed cleanly — &lt;code&gt;false&lt;/code&gt; is a perfectly valid value for that argument, the right type, in the right place. Nothing about the line was wrong in isolation; it just did not match reality. The only way to catch it by eye was to already know that this specific boolean is &lt;code&gt;ForceNew&lt;/code&gt;, and then to spot the &lt;code&gt;# forces replacement&lt;/code&gt; marker in several hundred lines of plan output for a resource with a dozen nested blocks.&lt;/p&gt;

&lt;p&gt;A policy reading the plan's action list needs none of that context. It does not need to know what &lt;code&gt;bootstrap_self_managed_addons&lt;/code&gt; means. It needs to know that &lt;code&gt;["delete","create"]&lt;/code&gt; is not allowed during an import. That is the strength of policy as code — it checks a structural invariant instead of relying on my attention span.&lt;/p&gt;

&lt;p&gt;The fix was a one-character change: flip it to &lt;code&gt;true&lt;/code&gt; to match the cluster as deployed, re-run, and the plan became a clean import. Found by a guardrail instead of by an incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one that was just drift
&lt;/h3&gt;

&lt;p&gt;During the security group phase, the soft-mandatory policy spoke up. A few in-place &lt;code&gt;update&lt;/code&gt; actions — not destructive, just minor differences between my generated config and the live rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rbbr163v7jfmblq0msi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rbbr163v7jfmblq0msi.png" alt="Soft-mandatory Sentinel policy warning about in-place updates" width="799" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Exactly the behaviour I wanted. The run paused, told me which resources it intended to modify, and waited. I checked each one, confirmed they were benign, overrode deliberately, and applied.&lt;/p&gt;

&lt;p&gt;The difference in handling between these two cases is the point of having enforcement levels. &lt;code&gt;hard-mandatory&lt;/code&gt; for "this must never happen", &lt;code&gt;soft-mandatory&lt;/code&gt; for "a human needs to look at this". Collapse both into a blocking rule and the import becomes unworkable. Collapse both into a warning and the EKS replacement goes through.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Every component of the app is now in Terraform state and managed through HCP Terraform. Around 40 resources across 23 types, including the one that needed &lt;code&gt;awscc&lt;/code&gt;. About 1,300 lines of validated HCL, with the &lt;code&gt;import&lt;/code&gt; blocks as a precise record of what was adopted and when.&lt;/p&gt;

&lt;p&gt;And the app never went down. Not because I was careful, though I tried to be, but because the system was set up so that carelessness would be caught.&lt;/p&gt;

&lt;p&gt;What I had at the end is a &lt;em&gt;managed&lt;/em&gt; application. What I did not have is &lt;em&gt;well-structured&lt;/em&gt; code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next: a 1,300-line &lt;code&gt;main.tf&lt;/code&gt; is not where this ends
&lt;/h2&gt;

&lt;p&gt;Let me be honest about what came out of this, because "I imported it successfully" papers over a lot.&lt;/p&gt;

&lt;p&gt;The file is a monolith. Every ID is hardcoded — &lt;code&gt;vpc-037c4dd02c90XXXXX&lt;/code&gt; appears a dozen times, security group IDs are inlined into the EKS cluster's &lt;code&gt;vpc_config&lt;/code&gt;, role ARNs are string literals. Resource names are generator artefacts (&lt;code&gt;stack_subnets_0&lt;/code&gt;, &lt;code&gt;iam_roles_32&lt;/code&gt;, &lt;code&gt;ingress_rules_in_stack_groups_1_5&lt;/code&gt;) that describe the query that found them, not what they do. The region repeats on nearly every resource.&lt;/p&gt;

&lt;p&gt;This is a reasonable place for an import to &lt;strong&gt;end&lt;/strong&gt;. It is an unreasonable place to &lt;strong&gt;stay&lt;/strong&gt;. You cannot reuse any of it for a second environment, and the hardcoded IDs mean Terraform does not actually know the subnets belong to the VPC — it just happens to have the right strings in the right places.&lt;/p&gt;

&lt;p&gt;So the next step is refactoring, and I plan to use &lt;strong&gt;HashiCorp's Agent Skills&lt;/strong&gt; again for it, since they encode HashiCorp's own module design best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decompose into &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;iam&lt;/code&gt;, &lt;code&gt;security&lt;/code&gt;, &lt;code&gt;eks&lt;/code&gt;, &lt;code&gt;alb&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;registry&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replace every hardcoded ID with a real reference, so the dependency graph reflects actual dependencies&lt;/li&gt;
&lt;li&gt;Promote region, environment name, and sizing to variables&lt;/li&gt;
&lt;li&gt;Name resources after their purpose, not their discovery order&lt;/li&gt;
&lt;li&gt;Define proper inputs and outputs at each module boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ID-to-reference conversion is the bulk of the work and the highest-value part, because it is what turns a snapshot of your infrastructure into a description of it. That is a post of its own.&lt;/p&gt;

&lt;p&gt;The framing I have settled on: &lt;strong&gt;import gets you &lt;em&gt;management&lt;/em&gt;; refactoring gets you &lt;em&gt;Infrastructure as Code&lt;/em&gt;.&lt;/strong&gt; Two different milestones, and worth being clear-eyed about which one you have reached.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would tell you before you try this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Discovery is now a reviewable artefact.&lt;/strong&gt; Moving from "paste IDs into a spreadsheet" to "a committed &lt;code&gt;.tfquery.hcl&lt;/code&gt; file" is a bigger shift than it looks. When someone adds a resource next month, I re-run the query instead of repeating the archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check support before you plan the work, not during it.&lt;/strong&gt; List resource coverage varies by provider and version. The &lt;a href="https://github.com/quixoticmonk/extract-provider-supported-actions/blob/main/list-resources-report.json" rel="noopener noreferrer"&gt;supported-resources report&lt;/a&gt; takes a minute to check and tells you whether this workflow is even available for the resources you care about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bring your resource IDs to the party.&lt;/strong&gt; Search and Import shows you managed and unmanaged resources together. Picking the right ones out of that list requires knowing their exact identifiers beforehand — and knowing which identifier each resource type uses, because it varies. Do that inventory before you open the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn the parent-pivot pattern.&lt;/strong&gt; Most child resources cannot be tagged. &lt;code&gt;for_each&lt;/code&gt; over a parent &lt;code&gt;list&lt;/code&gt; block's output is how you reach them. That one pattern unlocked routes, associations, SG rules, and listeners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ground your AI assistant in the registry, or don't use it.&lt;/strong&gt; An Agent Skill for conventions plus the MCP Server for version-specific docs is what made AI assistance trustworthy rather than a source of plausible-looking errors. "Check the docs, never guess, record what you couldn't cover" belongs in your prompt verbatim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read "starter configuration" literally.&lt;/strong&gt; The generator writes back everything the API returned — computed attributes, conflicting arguments, zero-value noise. Budget real time for cleanup. Still dramatically faster than hand-writing forty &lt;code&gt;import&lt;/code&gt; blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A clean &lt;code&gt;terraform validate&lt;/code&gt; is not safety.&lt;/strong&gt; The one I would underline. The gap between "valid configuration" and "safe plan" is exactly where brownfield imports go wrong, and exactly the gap policy as code fills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attach policies before the first import, not after the first scare.&lt;/strong&gt; Mine were in place before any resource entered state, which is why the EKS near-miss is an anecdote rather than an incident report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use both enforcement levels.&lt;/strong&gt; Hard for invariants that must never break, soft for things a human must consciously approve. One without the other gives you either a workflow you cannot complete or a warning nobody reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import in layers.&lt;/strong&gt; Smaller plans get read properly, dependencies resolve in order, failures stay localised. The extra runs cost minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The capability to import brownfield infrastructure has existed for years. What has been missing is not capability but &lt;em&gt;economics and confidence&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Search changes the economics — discovery becomes declarative and generation becomes bulk, so adopting forty resources is an afternoon rather than a sprint. Policy as code changes the confidence — you no longer have to be certain you read the plan correctly, because a machine checks the one invariant that matters on every run. MCP-grounded AI assistance makes the authoring in between fast without making it unreliable.&lt;/p&gt;

&lt;p&gt;The infrastructure that most needs to be in code is the infrastructure people are most afraid to touch. That fear is rational. It is also, I think, now addressable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo repo&lt;/strong&gt; (CloudFormation template, query file, prompts, app): &lt;a href="https://github.com/pranit-hashi/aws-brownfield-terraform-import" rel="noopener noreferrer"&gt;github.com/pranit-hashi/aws-brownfield-terraform-import&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel policies&lt;/strong&gt; used as guardrails: &lt;a href="https://github.com/pranit-hashi/import-sentinel-policies" rel="noopener noreferrer"&gt;github.com/pranit-hashi/import-sentinel-policies&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HashiCorp Agent Skills for Terraform&lt;/strong&gt; — highly recommended reading before you use them: &lt;a href="https://github.com/hashicorp/agent-skills/tree/main/plugins/terraform" rel="noopener noreferrer"&gt;github.com/hashicorp/agent-skills/tree/main/plugins/terraform&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform MCP Server&lt;/strong&gt;: &lt;a href="https://developer.hashicorp.com/terraform/mcp-server" rel="noopener noreferrer"&gt;developer.hashicorp.com/terraform/mcp-server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which resources support list/import?&lt;/strong&gt; An auto-updating report that tracks provider-level support, and the first thing I check before planning an import: &lt;a href="https://github.com/quixoticmonk/extract-provider-supported-actions/blob/main/list-resources-report.json" rel="noopener noreferrer"&gt;list-resources-report.json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Setup used: Terraform 1.14+, &lt;code&gt;hashicorp/aws&lt;/code&gt; 6.67.0 and &lt;code&gt;hashicorp/awscc&lt;/code&gt; 1.104.0, HCP Terraform with Sentinel policy sets, the Terraform MCP Server, and the &lt;code&gt;terraform-search-import&lt;/code&gt; Agent Skill.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you try this on your own brownfield app, I would like to hear which resource type caught you out. My money is on something in IAM.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>devops</category>
      <category>eks</category>
    </item>
    <item>
      <title>How to create a CodePipeline with source from another AWS account?</title>
      <dc:creator>Pranit Raje</dc:creator>
      <pubDate>Sun, 22 Mar 2020 15:22:30 +0000</pubDate>
      <link>https://dev.to/pranitraje/how-to-create-a-codepipeline-with-source-from-another-aws-account-n0m</link>
      <guid>https://dev.to/pranitraje/how-to-create-a-codepipeline-with-source-from-another-aws-account-n0m</guid>
      <description>&lt;p&gt;When using AWS services, more often than not, we need multiple AWS accounts for various reasons. If you work on AWS DevOps tools, you must have come across &lt;a href="https://aws.amazon.com/codepipeline/" rel="noopener noreferrer"&gt;AWS CodePipeline&lt;/a&gt; which is a pipeline automation tool from AWS. While working with CodePipeline in multi-account scenario, I'm sure you all have faced many issues like &lt;em&gt;Access Denied&lt;/em&gt; errors which is the most puzzling and frustrating error IMHO because it doesn't always say where we're lacking permissions. In multi-account scenario for CodePipeline, if you're deploying your application in another AWS account, AWS has this excellent &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create-cross-account.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; which I'd highly recommend you to go through. But what if you have a unique requirement when the source of your pipeline itself is in another AWS account and the deployment is in the same account as of CodePipeline? &lt;/p&gt;

&lt;p&gt;Recently, I came across this use-case and tried to find some starting guide to create a CodePipeline having source from another AWS account but couldn't find any useful resources. Hence, I decided to write this blog to help others who might run into such use-case.&lt;/p&gt;

&lt;p&gt;This blog will walk you through an example of deploying from &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;Amazon ECR&lt;/a&gt; to &lt;a href="https://aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;Amazon ECS&lt;/a&gt; with the help of CodePipeline. Here, I'm having a use-case in which,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Account-A has already-built Docker image hosted in ECR (Source stage)&lt;/li&gt;
&lt;li&gt;Account-B has S3 bucket (Source stage) containing &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/file-reference.html#pipelines-create-image-definitions" rel="noopener noreferrer"&gt;imagedefinitions.json&lt;/a&gt; and ECS (Deploy stage).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: All AWS resources in this example exist in the same AWS region.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our requirement is such that we have a team of developers in Account-A who will build their Docker images locally, push it in ECR repository in Account-A and this Docker image should be deployed automatically to the resources (ECS) present in Account-B whenever our developers make changes to their Docker images and push it in ECR repository of Account-A. And, we want our CodePipeline to be set up in Account-B itself where our deployment is done. Sounds challenging right? No worries. Let's get started...&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Two active &lt;a href="https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html" rel="noopener noreferrer"&gt;AWS accounts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A working &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/get-set-up-for-amazon-ecs.html" rel="noopener noreferrer"&gt;Amazon ECS cluster&lt;/a&gt; up and running with all other related resources e.g., &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/create-task-definition.html" rel="noopener noreferrer"&gt;Task Definition&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-create.html" rel="noopener noreferrer"&gt;ECR repository&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/create-service-console-v2.html" rel="noopener noreferrer"&gt;ECS Service&lt;/a&gt;, any Docker image in one of the AWS account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Target Architecture:
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fveblcjtmb550rq9b8q1u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fveblcjtmb550rq9b8q1u.png" alt="Target Architecture Diagram of Cross-Account CodePipeline where source is in another AWS account" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above diagram shows the following workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Initially, the developers will build the Docker image and push it to their central ECR repository in Account A.&lt;/li&gt;
&lt;li&gt;Account A will have a EventBridge Rule created listening to our ECR repository in Account A. As soon as a successful Docker image is pushed to our ECR repository, an EventBridge Rule will be triggered in Account A.&lt;/li&gt;
&lt;li&gt;Default EventBridge Bus in Account B will be configured as a target to our EventBridge rule in Account A.&lt;/li&gt;
&lt;li&gt;We will have another EventBridge rule created in Account B which will listen to the push events from the ECR repository in Account A.&lt;/li&gt;
&lt;li&gt;The EventBridge rule in Account B will trigger our CodePipeline.&lt;/li&gt;
&lt;li&gt;CodePipeline will assume a cross-account IAM role created in Account A to list and describe the latest Docker image pushed into ECR repository.&lt;/li&gt;
&lt;li&gt;CodePipeline will read the Docker image and pass it to the next stage of CodePipeline.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/file-reference.html#pipelines-create-image-definitions" rel="noopener noreferrer"&gt;imagedefinitions.zip&lt;/a&gt; file from S3 bucket will contain the information about the container name and the image URI of our Docker image stored in Account A’s ECR repository which will be used in ECS Standard deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, let's see how we can build such pipeline from AWS Management Console in a step-by-step manner, &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: IN ACCOUNT-B -&amp;gt; Set up a normal CodePipeline:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Firstly, create a standard pipeline having all the components of pipeline (ECR,S3,ECS) residing in same Account-B normally the way you would. We are creating this pipeline in the same account first just to get a backbone structure for our cross-account pipeline which we will modify according to our requirements in later steps.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In this pipeline, Source stage will have two actions i.e. ECR and S3 where ECR will be your repository with image tag and S3 bucket will be having zip file of the JSON file named imagedefinitions.json. This JSON file is required for the ECS deployment as it contains container name and the ECR image URI with its image tag. Refer to this &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/file-reference.html#pipelines-create-image-definitions" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; to know more about this file. Do remember to zip the file when S3 is used in the source stage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: This S3 bucket should be &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/user-guide/enable-versioning.html" rel="noopener noreferrer"&gt;versioned&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When you include ECS (Standard deployment) as a deployment provider in your pipeline, make sure to point S3 action's output as an input to ECS because ECR action's output will contain information only about the Docker image and not the container name which is expected by CodePipeline job worker for ECS. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once you have working pipeline in Account-B, we will modify it for cross-account deployments having source from Account-A. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: IN ACCOUNT-B -&amp;gt; Create custom AWS KMS key:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;For any cross-account deployments, CodePipeline require custom KMS key allowing another AWS account to access encrypted CodePipeline artifacts stored in CodePipeline’s S3 bucket (artifact store).&lt;/li&gt;
&lt;li&gt;Create custom AWS KMS key allowing Account-A to access CodePipeline artifacts residing in Account-B’s S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;While creating KMS policy via console, when you reach to the 5th step &lt;em&gt;Review and edit key policy&lt;/em&gt;, enter the below policy by replacing appropriate ARN at the commented places.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Id": "key-consolepolicy",
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Enable IAM User Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:root"  //MAKE CHANGES HERE
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "Allow access for Key Administrators",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:user/&amp;lt;YOUR_IAM_USER&amp;gt;"   //MAKE CHANGES HERE
            },
           "Action": [
                "kms:Create*",
                "kms:Describe*",
                "kms:Enable*",
                "kms:List*",
                "kms:Put*",
                "kms:Update*",
                "kms:Revoke*",
                "kms:Disable*",
                "kms:Get*",
                "kms:Delete*",
                "kms:TagResource",
                "kms:UntagResource",
                "kms:ScheduleKeyDeletion",
                "kms:CancelKeyDeletion"
            ],
            "Resource": "*"
        },
        {
            "Sid": "Allow use of the key",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:user/&amp;lt;YOUR_IAM_USER&amp;gt;",   //MAKE CHANGES HERE
                    "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:role/service-role/AWSCodePipelineServiceRole-&amp;lt;AWS_REGION&amp;gt;-&amp;lt;CODEPIPELINE_NAME&amp;gt;",  //REPLACE WITH CODEPIPELINE SERVICE ROLE ARN
                    "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:root"  //MAKE CHANGES HERE
                ]
            },
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "*"
        },
        {
            "Sid": "Allow attachment of persistent resources",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:user/&amp;lt;YOUR_IAM_USER&amp;gt;",   //MAKE CHANGES HERE
                    "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:role/service-role/AWSCodePipelineServiceRole-&amp;lt;AWS_REGION&amp;gt;-&amp;lt;CODEPIPELINE_NAME&amp;gt;",  //REPLACE WITH CODEPIPELINE SERVICE ROLE ARN
                    "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:root"  //MAKE CHANGES HERE
                ]
            },
            "Action": [
                "kms:CreateGrant",
                "kms:ListGrants",
                "kms:RevokeGrant"
            ],
            "Resource": "*",
            "Condition": {
                "Bool": {
                    "kms:GrantIsForAWSResource": "true"
                }
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The above KMS key policy will allow your IAM user, CodePipeline service role and Account-A to access this KMS key with which our artifacts will be encrypted.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: IN ACCOUNT-A -&amp;gt; Create a custom IAM role for cross-account access:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a new IAM role named &lt;em&gt;CrossAccount-A_Role&lt;/em&gt;. You can name this role anything you want as long as it follows the naming conventions in IAM. Consider giving the role a name that clearly states its purpose.&lt;/li&gt;
&lt;li&gt;On the &lt;em&gt;Create role&lt;/em&gt; page, choose &lt;em&gt;Another AWS account&lt;/em&gt; option from &lt;em&gt;Select type of trusted entity&lt;/em&gt; section. Enter the AWS Account ID of Account-B in the &lt;em&gt;Specify accounts that can use this role&lt;/em&gt; section. Click &lt;em&gt;Next: Permissions&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Attach two AWS managed policies named &lt;em&gt;AmazonEC2ContainerRegistryReadOnly&lt;/em&gt; and &lt;em&gt;AmazonS3FullAccess&lt;/em&gt; to this IAM Role. &lt;/li&gt;
&lt;li&gt;Create another custom inline IAM policy named &lt;em&gt;cross-account-KMS&lt;/em&gt; for this same IAM role to access Account-B’s KMS key which we created in Step 2. Enter the below IAM policy by replacing ARN of KMS key in Account-B at the commented place for this inline policy.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "kms:DescribeKey",
                "kms:GenerateDataKey*",
                "kms:Encrypt",
                "kms:ReEncrypt*",
                "kms:Decrypt"
            ],
            "Resource": [
                "&amp;lt;ARN_OF_KMS_KEY_IN_ACCOUNT-B&amp;gt;"  //MAKE CHANGE HERE
            ]
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: IN ACCOUNT-B -&amp;gt; Create a policy for the S3 bucket that grants access to Account A:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Choose your CodePipeline’s S3 bucket i.e. artifact store (not the source S3 bucket) and edit its Bucket policy according to the following policy by replacing appropriate ARN at the commented places.&lt;/li&gt;
&lt;li&gt;The below S3 bucket policy will allow Account-A to access our pipeline's artifacts.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Version": "2012-10-17",
    "Id": "SSEAndSSLPolicy",
    "Statement": [
    {
        "Sid": "DenyUnEncryptedObjectUploads",
        "Effect": "Deny",
        "Principal": "*",
        "Action": "s3:PutObject",
        "Resource": "arn:aws:s3:::&amp;lt;CODEPIPELINE'S_S3_BUCKET_NAME&amp;gt;/*",   //MAKE CHANGE HERE
        "Condition": {
                "StringNotEquals": {
                    "s3:x-amz-server-side-encryption": "aws:kms"
                }
            }
    },
    {
        "Sid": "DenyInsecureConnections",
        "Effect": "Deny",
        "Principal": "*",
        "Action": "s3:*",
        "Resource": "arn:aws:s3:::&amp;lt;CODEPIPELINE'S_S3_BUCKET_NAME&amp;gt;/*",   //MAKE CHANGE HERE
        "Condition": {
            "Bool": {
                    "aws:SecureTransport": false
                }
            }
    },
    {
        "Sid": "",
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:root"   //MAKE CHANGE HERE
            },
        "Action": [
                "s3:Get*",
                "s3:Put*"
            ],
        "Resource": "arn:aws:s3:::&amp;lt;CODEPIPELINE'S_S3_BUCKET_NAME&amp;gt;/*"   //MAKE CHANGE HERE
    },
    {
        "Sid": "",
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:root"   //MAKE CHANGE HERE
                },
        "Action": "s3:ListBucket",
        "Resource": "arn:aws:s3:::&amp;lt;CODEPIPELINE'S_S3_BUCKET_NAME&amp;gt;"   //MAKE CHANGE HERE
        }
    ]
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: IN ACCOUNT-B -&amp;gt; Create an inline policy for service role of CodePipeline:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;As we want CodePipeline service role to assume the cross-account IAM role we created in Step 3, we should add an extra inline IAM policy to the CodePipeline service role.&lt;/li&gt;
&lt;li&gt;Enter the below IAM policy by replacing Account-A ID at the commented place for this inline policy.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": [
            "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:role/*"   //MAKE CHANGE HERE
        ]
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: IN ACCOUNT-A -&amp;gt; Apply ECR Repository Policy:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;To allow CodePipeline in Account-B to pull ECR images residing in Account-A, ECR repository should allow Account-B to pull those images from its repository. For this, we will apply resource-based policy to our ECR repository.&lt;/li&gt;
&lt;li&gt;You can use the below ECR repository policy by replacing appropriate ARN at the commented places.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "Cross-account-policy",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::&amp;lt;ACCOUNT-B_ID&amp;gt;:root",   //MAKE CHANGE HERE
          "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:role/CrossAccount-A_Role"   //ARN OF CROSS-ACCOUNT IAM ROLE CREATED IN ACCOUNT-A AT STEP 3   
        ]
      },
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:CompleteLayerUpload",
        "ecr:DescribeImages",
        "ecr:DescribeRepositories",
        "ecr:GetDownloadUrlForLayer",
        "ecr:GetLifecyclePolicy",
        "ecr:GetLifecyclePolicyPreview",
        "ecr:GetRepositoryPolicy",
        "ecr:ListImages",
        "ecr:UploadLayerPart"
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: IN ACCOUNT-B -&amp;gt; Modify your pipeline’s configuration file:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Now that we have most of the required components of our cross-account pipeline ready, we will modify the configuration file of our pipeline that we created in Step 1 by changing the source to our ECR repository in Account-A and we will also add KMS related details.&lt;/li&gt;
&lt;li&gt;To do that, run get-pipeline command &lt;code&gt;aws codepipeline get-pipeline --name &amp;lt;CODEPIPELINE_NAME&amp;gt; --region &amp;lt;AWS_REGION&amp;gt; &amp;gt; pipeline.json&lt;/code&gt; from AWS CLI.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open this pipeline.json file in your favourite text editor (vim?) and add ‘encryptionKey’ in ‘artifactStore’ section.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; "artifactStore": {
    "type": "S3",
    "location": "codepipeline-&amp;lt;AWS_REGION&amp;gt;-XXXXXXXXXX",  //MAKE CHANGE HERE
    "encryptionKey": {
        "id": "arn:aws:kms:&amp;lt;AWS_REGION&amp;gt;:&amp;lt;ACCOUNT-B_ID&amp;gt;:key/XXXXXXXXXX-XXXX-XXX-XXXX-XXXXXXXXXX",    //MAKE CHANGE HERE    
        "type": "KMS"
        }
     }
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then, modify your ECR action of Source stage where you will change the values of ECR repository name and its image tag which is in Account-A.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the same ECR action, include the &lt;code&gt;"roleArn": "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:role/CrossAccount-A_Role"&lt;/code&gt; which we created in Step 3 so that this cross-account IAM role from Account-A will be used for ECR action. Following is the snippet for our cross-account source action in pipeline's JSON file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: Replace appropriate values at commented places below&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "name": "Source",
    "actionTypeId": {
      "category": "Source",
      "owner": "AWS",
      "provider": "ECR",
      "version": "1"
     },
    "runOrder": 1,
    "configuration": {
      "ImageTag": "latest",    //MAKE CHANGE HERE IN CASE OF CUSTOM TAG
      "RepositoryName": "XXXXX"    //MAKE CHANGE HERE
     },
    "outputArtifacts": [
      {
        "name": "SourceArtifact"
      }
     ],
    "inputArtifacts": [],
    "region": "&amp;lt;AWS_REGION&amp;gt;",  //MAKE CHANGE HERE
    "namespace": "SourceVariables",
    "roleArn": "arn:aws:iam::&amp;lt;ACCOUNT-A_ID&amp;gt;:role/CrossAccount-A_Role"  //MENTION CROSS-ACCOUNT IAM ROLE    
}
&lt;/code&gt;&lt;/pre&gt;


&lt;blockquote&gt;
&lt;p&gt;NOTE: Above is just a snippet of our ECR action of Source stage and not the entire JSON configuration of pipeline.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make sure to perform the 4th step of this &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create-cross-account.html#pipelines-create-cross-account-create" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; to remove the unwanted metadata section from configuration file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once you make the above changes, update the pipeline from update-pipeline CLI command &lt;code&gt;aws codepipeline update-pipeline --region &amp;lt;AWS_REGION&amp;gt; --cli-input-json file://pipeline.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;After you update your pipeline, don't forget to update your imagedefintions.json file with the correct imageURI from Account-A if you haven't done so. It should look like below,&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "name": "&amp;lt;CONTAINER_NAME&amp;gt;",  //MAKE CHANGES HERE
    "imageUri": "&amp;lt;ACCOUNT-A_ID&amp;gt;.dkr.ecr.&amp;lt;AWS_REGION&amp;gt;.amazonaws.com/&amp;lt;REPOSITORY_NAME&amp;gt;:&amp;lt;IMAGE_TAG&amp;gt;"  //MAKE CHANGES HERE    
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once updated the imagedefinitions.json file, zip it and upload it again to your source S3 bucket. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As you might be aware that &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html" rel="noopener noreferrer"&gt;CloudWatch Event (CWE)&lt;/a&gt; Rules are the resources in the background which trigger your pipeline whenever there are any changes to your source present in AWS. Since, one of our source action, i.e. ECR is existing in another AWS account, CodePipeline cannot create this CWE rule automatically for you for ECR as it did for S3 action. Hence, we will now create CWE rule for ECR repository with the help of &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEvents-CrossAccountEventDelivery.html" rel="noopener noreferrer"&gt;CloudWatch Event Bus&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: IN ACCOUNT-B -&amp;gt; Edit default Event Bus to allow account A:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to CloudWatch console and select your default Event Bus.&lt;/li&gt;
&lt;li&gt;Click on &lt;em&gt;Add permission&lt;/em&gt; to add your Account-A ID and check the &lt;em&gt;Everybody(*)&lt;/em&gt; option.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 9: IN ACCOUNT-A -&amp;gt; Create CWE Rule for your ECR repository:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Follow this &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/create-cwe-ecr-source-console.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; till &lt;strong&gt;Step 5&lt;/strong&gt; to create CWE rule for your ECR repository as a source for your CWE rule.&lt;/li&gt;
&lt;li&gt;As a target for this CWE rule, select &lt;em&gt;Event bus in another AWS account&lt;/em&gt;. Enter your Account-B ID and select &lt;em&gt;Create a new role for this specific resource&lt;/em&gt; option for IAM role which will automatically create IAM role for this CWE rule. &lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 10: IN ACCOUNT-B -&amp;gt; Create CWE Rule with CodePipeline as a target:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a new CWE rule with Event Source exactly same as you made in Account-A. Edit this event pattern by adding &lt;em&gt;account&lt;/em&gt; section with the value as Account-A’s ID. This will listen to any changes occurring to your ECR repository present in Account-A with the help of Event Bus. Refer to this &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEvents-CrossAccountEventDelivery.html#WritingRulesThatMatchEventsFromAnotherAccount" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; to know how to do this exactly. Once you create Event pattern, it should look like below,&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "detail-type": [
    "ECR Image Action"
  ],
  "account": [
    "ACCOUNT-A_ID"  //MAKE CHANGES HERE
  ],
  "source": [
    "aws.ecr"
  ],
  "detail": {
    "action-type": [
      "PUSH"
    ],
    "image-tag": [
      "latest"  //MAKE CHANGE HERE IN CASE OF CUSTOM TAG
    ],
    "repository-name": [
      "&amp;lt;REPOSITORY_NAME&amp;gt;"  //MAKE CHANGES HERE
    ],
    "result": [
      "SUCCESS"
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Put your CodePipeline ARN (&lt;code&gt;arn:aws:codepipeline:&amp;lt;AWS_REGION&amp;gt;:&amp;lt;ACCOUNT-B_ID&amp;gt;:&amp;lt;PIPELINE_NAME&amp;gt;&lt;/code&gt;) as a target for this CWE rule. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hooray!! Now, we have successfully set up CodePipeline in Account-B with ECR being in Account-A, S3 bucket containing only zipped imagedefinitions.json file for ECS deployment and ECS service in Account-B. Whenever you make any changes to any of your source, your pipeline will be triggered in a CI/CD manner. &lt;/p&gt;

&lt;h3&gt;
  
  
  Automation &amp;amp; Scale:
&lt;/h3&gt;

&lt;p&gt;Following the step-by-step instructions from the above section can be useful if we want to understand what we are doing to implement the architecture. However, once we understand that and when we want to automate the whole target architecture, it can be deployed in an automated way using &lt;a href="https://aws.amazon.com/cloudformation/" rel="noopener noreferrer"&gt;AWS CloudFormation&lt;/a&gt; which is an Infrastructure as Code (IaC) tool from AWS. To automate the deployment in multiple AWS accounts, make sure to use the CloudFormation templates from &lt;a href="https://github.com/StanForever/cross-account-codepipeline/" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. In the repository, we have three CloudFormation templates which should be deployed sequentially as noted below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Account B, create a new CloudFormation stack using AccB-pipeline-1.yaml template.&lt;/li&gt;
&lt;li&gt;In Account A, create a new CloudFormation stack using AccA-pipeline.yaml template.&lt;/li&gt;
&lt;li&gt;In Account B, run following AWS CLI command from Account B to allow Account A to send ECR push events to Event Bus in Account B.
&lt;code&gt;aws events put-permission --action events:PutEvents --statement-id MySid --principal &amp;lt;ACCOUNT_A_ID&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In Account B, update the CloudFormation stack created in Step 1 using AccB-pipeline-2.yaml template.  In this template, we will use one additional parameter named CrossAccountRoleName which will be the name of cross-account role created in Step 2. The IAM role name will be available from the Outputs section of CloudFormation console. Along with that, we will also put the ECR repository name created in Account A in Step 2 which is available in the Outputs section of CloudFormation console.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In Account B, make sure to update imagedefintions.json file with the correct imageURI from Account A. It may look like below,&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
{
"name": "&amp;lt;CONTAINER_NAME&amp;gt;",  //MAKE CHANGES HERE
"imageUri": "&amp;lt;ACCOUNT-A_ID&amp;gt;.dkr.ecr.&amp;lt;AWS_REGION&amp;gt;.amazonaws.com/&amp;lt;REPOSITORY_NAME&amp;gt;:&amp;lt;IMAGE_TAG&amp;gt;"  //MAKE CHANGES HERE    
}
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In Account A, push already built Docker image to ECR repository in Account A.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In Account B, notice how CodePipeline will get triggered automatically as soon as any Docker image is pushed to ECR repository in Account A. This Docker image will be deployed to ECS cluster in Account B in CI/CD manner.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks for reading this blog! I hope you will find this useful. Feel free to like, share and comment on this article with your constructive feedback. If you find this blog helpful or if you're stuck at any of the above step, do let me know via comments section and I'd be more than happy to help!&lt;/p&gt;

&lt;p&gt;Have an AWSome Day! 😎&lt;/p&gt;

</description>
      <category>codepipeline</category>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
