{"id":2137,"date":"2026-02-21T03:12:59","date_gmt":"2026-02-20T14:12:59","guid":{"rendered":"https:\/\/www.ronella.xyz\/?p=2137"},"modified":"2026-02-21T03:12:59","modified_gmt":"2026-02-20T14:12:59","slug":"understanding-state-with-show-state-and-output","status":"publish","type":"post","link":"https:\/\/www.ronella.xyz\/?p=2137","title":{"rendered":"Understanding State with show, state, and output"},"content":{"rendered":"<p>Terraform\u2019s <strong>state<\/strong> is how it \u201cremembers\u201d what exists in your infrastructure so it can plan precise, minimal changes instead of blindly recreating resources. In this article, we\u2019ll treat Terraform as a black box and learn how to <em>inspect<\/em> its memory using three key CLI tools: <code>terraform show<\/code>, the <code>terraform state<\/code> subcommands, and <code>terraform output<\/code>.<\/p>\n<hr \/>\n<h2>1. What Terraform State Actually Is<\/h2>\n<p>Terraform keeps a mapping between:<\/p>\n<ul>\n<li>Your configuration (<code>.tf<\/code> files)<\/li>\n<li>The real resources in your cloud provider (IDs, IPs, ARNs, etc.)<\/li>\n<\/ul>\n<p>This mapping lives in a state file, usually <code>terraform.tfstate<\/code>, and often in a remote backend such as S3, Azure Blob Storage, or GCS for team use. The state includes every attribute of every managed resource, plus metadata used for things like dependency ordering and change detection.<\/p>\n<p><strong>Why you care:<\/strong><\/p>\n<ul>\n<li>Debugging: Is Terraform seeing the same thing you see in the console?<\/li>\n<li>Refactoring: How do you rename resources without destroying them?<\/li>\n<li>Automation: How do you feed outputs into CI\/CD or other tools?<\/li>\n<\/ul>\n<p>You should <strong>never<\/strong> hand-edit the state file; instead you use the CLI commands discussed below to read or safely modify it.<\/p>\n<hr \/>\n<h2>2. <code>terraform show<\/code> \u2014 Inspecting the Whole State or a Plan<\/h2>\n<p>Think of <code>terraform show<\/code> as \u201cdump what Terraform currently knows\u201d \u2014 it turns a state file or a saved plan into a human-readable or JSON view.<\/p>\n<h2>Core usage<\/h2>\n<pre><code class=\"language-bash\"># Show the current state snapshot (from the active backend)\nterraform show\n\n# Show a specific state file\nterraform show path\/to\/terraform.tfstate\n\n# Show a saved plan file\nterraform show tfplan\n\n# Machine-readable JSON for tooling\nterraform show -json &gt; plan.json<\/code><\/pre>\n<ul>\n<li>Without a file argument, <code>terraform show<\/code> prints the latest state snapshot from the active backend.<\/li>\n<li>With a plan file, it describes the proposed actions and resulting state.<\/li>\n<li>With <code>-json<\/code>, you get a structured document that external tools (e.g. CI, tests) can parse and validate.<\/li>\n<\/ul>\n<blockquote>\n<p>Important: When using <code>-json<\/code>, <strong>sensitive values are printed in plain text<\/strong>; handle this carefully in pipelines and logs.<\/p>\n<\/blockquote>\n<h2>When to use <code>terraform show<\/code><\/h2>\n<p>Use it when:<\/p>\n<ul>\n<li>You want a <strong>global view<\/strong>: \u201cWhat exactly is Terraform tracking right now?\u201d<\/li>\n<li>You want to <strong>inspect a plan artifact<\/strong> (<code>plan -out tfplan<\/code>) before approving it in CI.<\/li>\n<li>You want to feed state or plan data into a tool (via <code>-json<\/code>) for policy checks, drift checks, or custom validation.<\/li>\n<\/ul>\n<p>Conceptually, <code>terraform show<\/code> is read-only and holistic: it treats the state (or plan) as a whole, rather than individual resources.<\/p>\n<hr \/>\n<h2>3. <code>terraform state<\/code> \u2014 Fine-Grained State Inspection and Surgery<\/h2>\n<p>The <code>terraform state<\/code> command is a <strong>group of subcommands<\/strong> designed specifically to <em>inspect and modify<\/em> state without touching real infrastructure. This is the surgical toolkit you reach for when refactoring or repairing.<\/p>\n<h2>Key subcommands<\/h2>\n<table>\n<thead>\n<tr>\n<th>Command<\/th>\n<th>What it does<\/th>\n<th>Typical use<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>terraform state list<\/code><\/td>\n<td>Lists all resource addresses in state<\/td>\n<td>\u201cWhat is Terraform tracking?\u201d<\/td>\n<\/tr>\n<tr>\n<td><code>terraform state show ADDRESS<\/code><\/td>\n<td>Shows attributes of one resource<\/td>\n<td>Debugging one resource (IDs, IPs, tags, etc.)<\/td>\n<\/tr>\n<tr>\n<td><code>terraform state mv SRC DEST<\/code><\/td>\n<td>Moves\/renames a resource in state<\/td>\n<td>Refactors config without destroy\/recreate<\/td>\n<\/tr>\n<tr>\n<td><code>terraform state rm ADDRESS<\/code><\/td>\n<td>Removes a resource from state<\/td>\n<td>Stop managing a resource without deleting it<\/td>\n<\/tr>\n<tr>\n<td><code>terraform state pull<\/code><\/td>\n<td>Prints raw state to stdout<\/td>\n<td>Backup, inspection, or external processing<\/td>\n<\/tr>\n<tr>\n<td><code>terraform state push<\/code><\/td>\n<td>Uploads a local state file<\/td>\n<td>Restore\/correct broken remote state (used rarely, carefully)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>3.1 <code>terraform state list<\/code><\/h2>\n<pre><code class=\"language-bash\">terraform state list\n# e.g.\n# aws_instance.web[0]\n# aws_instance.web[1]\n# aws_security_group.allow_ssh<\/code><\/pre>\n<p>This gives you the <strong>resource addresses<\/strong> Terraform knows about, optionally filtered by a prefix. It\u2019s extremely useful when working with modules or count\/for_each, because you can see the exact address Terraform expects.<\/p>\n<h2>3.2 <code>terraform state show<\/code><\/h2>\n<pre><code class=\"language-bash\">terraform state show aws_instance.web[0]<\/code><\/pre>\n<p>This prints every attribute of that specific resource as seen in state \u2014 IDs, IPs, tags, relationships, and computed attributes. Semantically, it answers: <em>\u201cWhat does Terraform think this one resource looks like?\u201d<\/em>.<\/p>\n<p>Use it when:<\/p>\n<ul>\n<li>Debugging drift: console vs state mismatch.<\/li>\n<li>Understanding complex resources: which subnet, which IAM role?<\/li>\n<li>Checking data sources that were resolved at apply time.<\/li>\n<\/ul>\n<p>Note the difference:<\/p>\n<ul>\n<li><code>terraform show<\/code> \u2192 everything (or full plan).<\/li>\n<li><code>terraform state show ADDRESS<\/code> \u2192 one resource only.<\/li>\n<\/ul>\n<h2>3.3 <code>terraform state mv<\/code> \u2014 Refactor Without Downtime<\/h2>\n<pre><code class=\"language-bash\">terraform state mv aws_instance.web aws_instance.app<\/code><\/pre>\n<p>If you simply rename the block in your <code>.tf<\/code> code, Terraform will plan to <strong>destroy<\/strong> the old resource and <strong>create<\/strong> a new one because it assumes they\u2019re unrelated. <code>state mv<\/code> tells Terraform that the <em>underlying resource<\/em> is the same, you\u2019re just changing the mapping.<\/p>\n<p>This is critical for:<\/p>\n<ul>\n<li>Renaming resources.<\/li>\n<li>Moving resources into\/out of modules.<\/li>\n<li>Splitting a monolith configuration into multiple modules\/workspaces.<\/li>\n<\/ul>\n<h2>3.4 <code>terraform state rm<\/code> \u2014 Stop Managing Without Deleting<\/h2>\n<pre><code class=\"language-bash\">terraform state rm aws_instance.legacy<\/code><\/pre>\n<p>This removes the resource from Terraform\u2019s management while leaving it alive in your provider. Use this when decommissioning Terraform from part of your estate or when you temporarily need Terraform to \u201cforget\u201d something (e.g. migration to a different tool).<\/p>\n<h2>3.5 <code>terraform state pull<\/code> \/ <code>push<\/code><\/h2>\n<p>These expose and manipulate the raw state blob:<\/p>\n<pre><code class=\"language-bash\">terraform state pull &gt; backup.tfstate\nterraform state push backup.tfstate<\/code><\/pre>\n<p>They\u2019re useful for backups or extremely rare recovery scenarios, but they\u2019re <strong>dangerous<\/strong> if misused, so in practice you rely much more on <code>list<\/code>, <code>show<\/code>, <code>mv<\/code>, and <code>rm<\/code>.<\/p>\n<hr \/>\n<h2>4. <code>terraform output<\/code> \u2014 Consuming State Safely<\/h2>\n<p><code>terraform output<\/code> reads <strong>output values<\/strong> defined in the root module and prints their values from the state file. It is the \u201cofficial interface\u201d for other systems (and humans) to consume selected bits of state without parsing the state file directly.<\/p>\n<h2>4.1 Defining outputs in configuration<\/h2>\n<p>In your root module:<\/p>\n<pre><code class=\"language-bash\">output &quot;instance_ips&quot; {\n  value = aws_instance.web[*].public_ip\n}\n\noutput &quot;lb_address&quot; {\n  value = aws_lb.web.dns_name\n}\n\noutput &quot;db_connection_string&quot; {\n  value     = module.database.connection_string\n  sensitive = true\n}<\/code><\/pre>\n<ul>\n<li>Outputs are calculated after <code>terraform apply<\/code> and stored in state.<\/li>\n<li>Only <strong>root module<\/strong> outputs are visible to <code>terraform output<\/code>; child module outputs must be re-exposed.<\/li>\n<\/ul>\n<h2>4.2 Using <code>terraform output<\/code> interactively<\/h2>\n<pre><code class=\"language-bash\"># Show all outputs for the root module\nterraform output\n\n# Show one specific output\nterraform output lb_address\n\n# Machine-readable JSON\nterraform output -json\n\n# Raw string (no quotes\/newlines), perfect for scripts\nterraform output -raw lb_address<\/code><\/pre>\n<ul>\n<li>With no arguments, it prints all root outputs.<\/li>\n<li>With a <code>NAME<\/code>, it prints just that value.<\/li>\n<li><code>-json<\/code> gives a JSON object keyed by output name; can be piped into <code>jq<\/code> or similar tools.<\/li>\n<li><code>-raw<\/code> prints a bare string\/number\/boolean; ideal when exporting in shell scripts without extra quoting.<\/li>\n<\/ul>\n<p>This is the idiomatic way to feed state into:<\/p>\n<ul>\n<li>CI\/CD pipelines (e.g. get ALB DNS for integration tests).<\/li>\n<li>Other scripts (e.g. configure DNS records).<\/li>\n<li>Other tools (e.g. Ansible inventory).<\/li>\n<\/ul>\n<hr \/>\n<h2>5. Putting It Together: A Simple Example<\/h2>\n<p>Below is a minimal, self-contained configuration you can run locally.<\/p>\n<h2>5.1. Prerequisites<\/h2>\n<ol>\n<li>\n<p>LocalStack running (Docker is typical):<\/p>\n<pre><code class=\"language-bash\">docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack\/localstack<\/code><\/pre>\n<p>LocalStack\u2019s edge endpoint is exposed on <code>http:\/\/localhost:4566<\/code> by default.<\/p>\n<\/li>\n<li>\n<p>Terraform installed (1.x).<\/p>\n<\/li>\n<\/ol>\n<h2>5.2. Terraform Configuration Using LocalStack<\/h2>\n<p>Create a directory (for example <code>tf-localstack-ec2<\/code>) and within it create two files: <code>versions.tf<\/code> and <code>main.tf<\/code>.<\/p>\n<h4><code>versions.tf<\/code><\/h4>\n<p>Lock AWS provider to a version that is known to work well with LocalStack (4.x is a common choice):<\/p>\n<pre><code class=\"language-terraform\">terraform {\n  required_version = &quot;&gt;= 1.6.0&quot;\n\n  required_providers {\n    aws = {\n      source  = &quot;hashicorp\/aws&quot;\n      version = &quot;~&gt; 5.0&quot;\n    }\n  }\n}<\/code><\/pre>\n<h4><code>main.tf<\/code><\/h4>\n<pre><code class=\"language-bash\">provider &quot;aws&quot; {\n  region                      = &quot;us-east-1&quot;\n  access_key                  = &quot;test&quot;\n  secret_key                  = &quot;test&quot;\n  skip_credentials_validation = true\n  skip_metadata_api_check     = true\n  skip_requesting_account_id  = true\n\n  endpoints {\n    ec2 = &quot;http:\/\/localhost:4566&quot;\n  }\n}\n\nresource &quot;aws_instance&quot; &quot;web&quot; {\n  ami           = &quot;ami-12345678&quot;\n  instance_type = &quot;t3.micro&quot;\n\n  tags = {\n    Name = &quot;tf-demo-web-localstack&quot;\n  }\n}\n\noutput &quot;web_public_ip&quot; {\n  value = aws_instance.web.public_ip\n}<\/code><\/pre>\n<p>Notes:<\/p>\n<ul>\n<li>The <code>endpoints.ec2<\/code> block points the EC2 API at LocalStack\u2019s edge endpoint.<\/li>\n<li>Credentials are dummy; LocalStack doesn\u2019t actually validate them.<\/li>\n<li>The AMI ID is a placeholder; LocalStack typically does not require the AMI to exist, but EC2 support is limited and can hang for some combinations. For state\/command learning it\u2019s usually enough that Terraform \u201cthinks\u201d it created something.<\/li>\n<\/ul>\n<h2>5.3. How to Apply and Validate<\/h2>\n<p>From the directory containing these files:<\/p>\n<ol>\n<li>\n<p><strong>Initialize and apply<\/strong><\/p>\n<pre><code class=\"language-bash\">terraform init\nterraform apply -auto-approve<\/code><\/pre>\n<p>Terraform will talk to LocalStack instead of AWS because of the custom endpoint.<\/p>\n<\/li>\n<li>\n<p><strong>Validate with <code>show<\/code><\/strong><\/p>\n<pre><code class=\"language-bash\">terraform show<\/code><\/pre>\n<p>Confirm there is a <code>aws_instance.web<\/code> block with attributes populated from LocalStack\u2019s response.<\/p>\n<\/li>\n<li>\n<p><strong>Validate with <code>state<\/code><\/strong><\/p>\n<pre><code class=\"language-bash\">terraform state list\n# should include:\n# aws_instance.web\n\nterraform state show aws_instance.web<\/code><\/pre>\n<p>This tells you what Terraform\u2019s state holds for this specific resource address.<\/p>\n<\/li>\n<li>\n<p><strong>Validate with <code>output<\/code><\/strong><\/p>\n<pre><code class=\"language-bash\">terraform output web_public_ip\nterraform output -raw web_public_ip<\/code><\/pre>\n<p>For LocalStack, the public IP may be empty or synthetic depending on EC2 emulation level, but the command proves the wiring from resource \u2192 state \u2192 output.<\/p>\n<\/li>\n<\/ol>\n<h2>5.4. Rationale for These Choices<\/h2>\n<ul>\n<li>We override only the EC2 endpoint to keep the example close to \u201creal\u201d AWS code while still talking to LocalStack.<\/li>\n<li>We relax provider validations (<code>skip_*<\/code> flags) because LocalStack does not implement all AWS account\/metadata APIs.<\/li>\n<\/ul>\n<p>With this setup, you can safely experiment with <code>terraform show<\/code>, <code>terraform state *<\/code>, and <code>terraform output<\/code> on your laptop, without touching real AWS accounts or incurring cost.<\/p>\n<hr \/>\n<h2>6. Conceptual Summary: Which Command When?<\/h2>\n<table>\n<thead>\n<tr>\n<th>Need<\/th>\n<th>Command<\/th>\n<th>Rationale<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>See everything Terraform knows<\/td>\n<td><code>terraform show<\/code><\/td>\n<td>Whole-state, read-only view (or a plan)<\/td>\n<\/tr>\n<tr>\n<td>Inspect one resource deeply<\/td>\n<td><code>terraform state show ADDRESS<\/code><\/td>\n<td>Focused, per-resource state inspection<\/td>\n<\/tr>\n<tr>\n<td>List all tracked resources<\/td>\n<td><code>terraform state list<\/code><\/td>\n<td>Discover resource addresses in state<\/td>\n<\/tr>\n<tr>\n<td>Rename\/move resources\/modules<\/td>\n<td><code>terraform state mv<\/code><\/td>\n<td>Refactor mappings without downtime<\/td>\n<\/tr>\n<tr>\n<td>Forget a resource but keep it alive<\/td>\n<td><code>terraform state rm<\/code><\/td>\n<td>Stop managing without deleting<\/td>\n<\/tr>\n<tr>\n<td>Give other tools a clean interface<\/td>\n<td><code>terraform output<\/code> \/ <code>-json<\/code> \/ <code>-raw<\/code><\/td>\n<td>Official way to expose selected state data developer.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The underlying rationale is separation of concerns:<\/p>\n<ul>\n<li><code>terraform show<\/code> \u2192 <em>observability of plans and state<\/em>.<\/li>\n<li><code>terraform state<\/code> \u2192 <em>precise manipulation and inspection of state<\/em>.<\/li>\n<li><code>terraform output<\/code> \u2192 <em>controlled, stable API to state for humans and downstream systems<\/em>.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Terraform\u2019s state is how it \u201cremembers\u201d what exists in your infrastructure so it can plan precise, minimal changes instead of blindly recreating resources. In this article, we\u2019ll treat Terraform as a black box and learn how to inspect its memory using three key CLI tools: terraform show, the terraform state subcommands, and terraform output. 1. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[94],"tags":[],"_links":{"self":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2137"}],"collection":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2137"}],"version-history":[{"count":1,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2137\/revisions"}],"predecessor-version":[{"id":2138,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2137\/revisions\/2138"}],"wp:attachment":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2137"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2137"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2137"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}